C-更好的开关还是const表?(嵌入式SW)



我想知道是否在可能的时候是更有效的开关还是const表?

例如,什么会更好:

switch(input) {
  case 0: value = VALUE_0;
    break;
  case 1: value = VALUE_1;
    break;
  case 2: value = VALUE_2;
    break;
  case 3: value = VALUE_3;
    break;
  case 4: value = VALUE_4;
    break;
  case 5: value = VALUE_5;
    break;
  case 6: value = VALUE_6;
    break;
  default:    
    break;
}

或类似的东西:

const uint8_t INPUT_TO_VALUE_TABLE[N_VALUE] = {
    VALUE_0,
    VALUE_1,
    VALUE_2,
    VALUE_3,
    VALUE_4,
    VALUE_5,
    VALUE_6,
}
...
...
value = INPUT_TO_VALUE_TABLE[input];

我显示了一个虚拟示例,但是我也有用于使用开关调用不同函数或功能指针表的代码。

代码用于8bits micro(我不知道这对此主题有任何区别)。

好吧,您应该考虑拆卸编译的代码以查看实际生成的内容,但我希望您在第二种情况下最终会更少的代码,并且分支更少。<<<<<<<<<<<<<<

在第一种情况下,有七个分配语句,还有一堆跳跃(从开关语句中)。在第二个中,有一个数组参考和一个作业。由于您的案例都是连续的,因此很容易处理默认情况:

value = ( input < 6 ) ? INPUT_TO_VALUE_TABLE[input] : default_value;

让我们看一下一些组件。这是用gcc -S,版本4.6.3编译的,因此它与您要获得的组件不同,但我们应该得到相同的一般结果。这个答案不会绝对回答以下问题的问题。您必须自己进行一些测试,但是看起来很可取。

switch选项:

我们将从switch开始:

void switch_input( int input ) {
  switch(input) {
  case 0: value = VALUE_0;
    break;
  case 1: value = VALUE_1;
    break;
  case 2: value = VALUE_2;
    break;
  case 3: value = VALUE_3;
    break;
  case 4: value = VALUE_4;
    break;
  case 5: value = VALUE_5;
    break;
  case 6: value = VALUE_6;
    break;
  default: value = VALUE_DEFAULT;
    break;
  }
}

这有很多跳跃,因为有七个不同的作业,并且基于input的值,我们必须能够跳到每个分配,然后跳到switch的末端。

switch_input:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    cmpl    $6, -4(%rbp)
    ja  .L2
    movl    -4(%rbp), %eax
    movq    .L10(,%rax,8), %rax
    jmp *%rax
    .section    .rodata
    .align 8
    .align 4
.L10:
    .quad   .L3
    .quad   .L4
    .quad   .L5
    .quad   .L6
    .quad   .L7
    .quad   .L8
    .quad   .L9
    .text
.L3:
    movl    $0, value(%rip)
    jmp .L1
.L4:
    movl    $1, value(%rip)
    jmp .L1
.L5:
    movl    $2, value(%rip)
    jmp .L1
.L6:
    movl    $3, value(%rip)
    jmp .L1
.L7:
    movl    $4, value(%rip)
    jmp .L1
.L8:
    movl    $5, value(%rip)
    jmp .L1
.L9:
    movl    $6, value(%rip)
    jmp .L1
.L2:
    movl    $-1, value(%rip)
    nop
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

表选项

表选项只能使用一个分配,而是拥有大量代码可以跳到的代码,我们只需要一个值表即可。我们也不需要跳入该桌子。我们只需要计算一个索引,然后无条件地从中加载一个值。

void index_input( int input ) {
  value = ( input < N_VALUE ) ? INPUT_TO_VALUE_TABLE[input] : VALUE_DEFAULT;
}

(是的,我们真的应该在那里使用一个未签名的整数,以便我们知道它不会少于零。)

index_input:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    cmpl    $5, -4(%rbp)
    jg  .L13
    movl    -4(%rbp), %eax
    cltq
    movl    INPUT_TO_VALUE_TABLE(,%rax,4), %eax
    jmp .L14
.L13:
    movl    $-1, %eax
.L14:
    movl    %eax, value(%rip)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

附录

C代码(example.c)

int value;
#define N_VALUE 7
#define VALUE_0 0
#define VALUE_1 1
#define VALUE_2 2
#define VALUE_3 3
#define VALUE_4 4
#define VALUE_5 5
#define VALUE_6 6
#define VALUE_DEFAULT -1
void switch_input( int input ) {
  switch(input) {
  case 0: value = VALUE_0;
    break;
  case 1: value = VALUE_1;
    break;
  case 2: value = VALUE_2;
    break;
  case 3: value = VALUE_3;
    break;
  case 4: value = VALUE_4;
    break;
  case 5: value = VALUE_5;
    break;
  case 6: value = VALUE_6;
    break;
  default: value = VALUE_DEFAULT;
    break;
  }
}
const int INPUT_TO_VALUE_TABLE[N_VALUE] = {
  VALUE_0,
  VALUE_1,
  VALUE_2,
  VALUE_3,
  VALUE_4,
  VALUE_5,
  VALUE_6
};
void index_input( int input ) {
  value = ( input < 6 ) ? INPUT_TO_VALUE_TABLE[input] : VALUE_DEFAULT;
}

汇编(示例)

gcc -S

生成
    .file   "example.c"
    .comm   value,4,4
    .text
    .globl  switch_input
    .type   switch_input, @function
switch_input:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    cmpl    $6, -4(%rbp)
    ja  .L2
    movl    -4(%rbp), %eax
    movq    .L10(,%rax,8), %rax
    jmp *%rax
    .section    .rodata
    .align 8
    .align 4
.L10:
    .quad   .L3
    .quad   .L4
    .quad   .L5
    .quad   .L6
    .quad   .L7
    .quad   .L8
    .quad   .L9
    .text
.L3:
    movl    $0, value(%rip)
    jmp .L1
.L4:
    movl    $1, value(%rip)
    jmp .L1
.L5:
    movl    $2, value(%rip)
    jmp .L1
.L6:
    movl    $3, value(%rip)
    jmp .L1
.L7:
    movl    $4, value(%rip)
    jmp .L1
.L8:
    movl    $5, value(%rip)
    jmp .L1
.L9:
    movl    $6, value(%rip)
    jmp .L1
.L2:
    movl    $-1, value(%rip)
    nop
.L1:
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   switch_input, .-switch_input
    .globl  INPUT_TO_VALUE_TABLE
    .section    .rodata
    .align 16
    .type   INPUT_TO_VALUE_TABLE, @object
    .size   INPUT_TO_VALUE_TABLE, 28
INPUT_TO_VALUE_TABLE:
    .long   0
    .long   1
    .long   2
    .long   3
    .long   4
    .long   5
    .long   6
    .text
    .globl  index_input
    .type   index_input, @function
index_input:
.LFB1:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    %edi, -4(%rbp)
    cmpl    $5, -4(%rbp)
    jg  .L13
    movl    -4(%rbp), %eax
    cltq
    movl    INPUT_TO_VALUE_TABLE(,%rax,4), %eax
    jmp .L14
.L13:
    movl    $-1, %eax
.L14:
    movl    %eax, value(%rip)
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE1:
    .size   index_input, .-index_input
    .ident  "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

可能不太可能将开关优化成与桌子查找一样高效的东西。可以肯定地假设表更快,但是您必须检查特定系统。

如果它是一系列功能指针,每个功能指针代表case,那将是另一个故事。因为一个不错的编译器很可能优化了一个相邻数字输入到此类函数点表中的开关。

由于您提到了8位MCUS,因此对手动代码进行优化可能非常相关。可悲的是,大多数主流8位MCU编译器在代码优化时较差(并且在标准合规性方面通常也很差)。

第二个更有效,但缺乏溢出保护。除非您确定input06之间,则需要一个警卫:

if (input >= 0 && input < 7) {
    value = INPUT_TO_VALUE_TABLE[input];
}

如果您需要警卫,它将消耗一些性能优势。

它取决于应用程序。我更喜欢开关,因为如果所有情况失败,我们都可以使用默认情况来执行任何操作。

使用本地表肯定在CPU周期(CPE)和内存消耗中更为保守。您只需查看其装配代码即可轻松推导它。

您可以在此处找到

的其他说明

这取决于,您应该为它计时。像您这样的基于紧凑的基于0的表可以具有 if作为后卫:

if ((unsigned)input <= 6) {
   value = INPUT_TO_VALUE_TABLE[input];
}

(因为input的负值在这里被视为大正值)

如果您不需要警卫,则桌子很可能是最快的。如果您需要...这取决于。

在某些硬件上,使用单个if(而不是2)检查范围也很有用:

也很有用
value = (unsigned)(input - min) <= max - min ? table[input] : default_value;

(显然,如果最大和最小是常数,或者至少可以预先计算max - min

最新更新