C 标签作为值与开关语句

C 标签作为值与开关语句,c,gcc,C,Gcc,我最近读到关于标签作为值 int main(){ int value = 2; const void *labels[] = {&&val_0, &&val_1, &&val_2}; goto *labels[value]; val_0: printf("The value is 0\n"); goto end; val_1: printf("The valu

我最近读到关于标签作为值

int main(){
    int value  = 2;
    const void *labels[] = {&&val_0, &&val_1, &&val_2};
    goto *labels[value];
    val_0:
        printf("The value is 0\n");
        goto end;
    val_1:
        printf("The value is 1\n");
        goto end;
    val_2:
        printf("The value is 2\n");
        goto end;
    end:
    return(0);
}

我想问的是,使用这种方法而不是使用switch语句或指针数组,是否真的会提高性能?

这是一种非标准扩展,在这种情况下,它的性能可能不会比等效switch语句好,因此,IMHO,应该避免。switch语句更清晰,更易于维护

(我很快测试了我的gcc版本,它为这段代码和switch语句生成了完全相同的代码。不过,这不是一个具有代表性的测试,因为它优化了除实际选择的代码路径之外的所有内容。)


一个潜在的性能考虑因素是,switch语句必须具有合理的行为,即使
value
不在正确的范围内,您的版本具有未定义的行为,因此编译器可以避免在某些代码中进行范围检查。

我认为switch语句更具可读性和更清晰,因此,除非问题不适合switch语句,否则请使用它而不是数组

标签为值的最佳用途是在线程代码的解释器中:

解释器功能中的标签可以存储在 超级快速调度的线程代码


下面是gnu gcc手册中关于此功能的文档
然而,有效编译
开关仍然是一个悬而未决的问题,请参见例如


虽然label as values和indirect
goto
-s确实是一种GCC语言扩展,但它已经被其他编译器(
icc
,LLVM/
clang
)采用,并且确实很有用(并且是一种低级功能,本着C的精神),特别是用于编码线程代码解释器、自动机等。。。。我不知道为什么该扩展没有成为标准(我猜它没有标准化是因为社会或经济因素,而不是因为技术无用。)

答案在于gcc生成的汇编代码(
-g
-O2
)。这两个函数将
value
作为参数,首先要求用户使用它(强制gcc不要消除未使用的代码片段-也称为死代码消除)。当然,
printf
部分对于函数和gcc优化(两者)都是相同的,因此它在打印后立即返回。因此,重要的部分是这两个功能的开始。让我们来看看:

转到功能:
0x080484d0 <+0>:    push   %ebp                      # -
0x080484d1 <+1>:    mov    %esp,%ebp                 #  |- standard prologue
0x080484d3 <+3>:    sub    $0x28,%esp                # -
0x080484d6 <+6>:    mov    0x8(%ebp),%eax            # get argument
0x080484d9 <+9>:    movl   $0x80484f8,-0x14(%ebp)    # set up labels array
0x080484e0 <+16>:   movl   $0x8048510,-0x10(%ebp)
0x080484e7 <+23>:   movl   $0x8048528,-0xc(%ebp)
0x080484ee <+30>:   jmp    *-0x14(%ebp,%eax,4)       # jump to appropriate sect.
0x080484f2 <+34>:   lea    0x0(%esi),%esi
0x08048470 <+0>:    push   %ebp                      # -
0x08048471 <+1>:    mov    %esp,%ebp                 #  |- standard prologue
0x08048473 <+3>:    sub    $0x18,%esp                # -
0x08048476 <+6>:    mov    0x8(%ebp),%eax            # get argument
0x08048479 <+9>:    cmp    $0x1,%eax
0x0804847c <+12>:   je     0x80484b8 <switchFunc+72> # jump here if value == 1
0x0804847e <+14>:   cmp    $0x2,%eax
0x08048481 <+17>:   je     0x80484a0 <switchFunc+48> # if value == 2
0x08048483 <+19>:   test   %eax,%eax
0x08048485 <+21>:   jne    0x804849b <switchFunc+43> # if value != 0 return
0x08048400:push%ebp#-
0x080484d1:mov%esp,%ebp#|-标准开场白
0x080484d3:sub$0x28,%esp#-
0x080484d6:mov 0x8(%ebp),%eax#get参数
0x080484d9:movl$0x80484f8,-0x14(%ebp)#设置标签数组
0x080484e0:movl$0x8048510,-0x10(%ebp)
0x080484e7:movl$0x8048528,-0xc(%ebp)
0x080484ee:jmp*-0x14(%ebp,%eax,4)#跳转到适当的节。
0x080484f2:lea 0x0(%esi),%esi
开关功能:
0x080484d0 <+0>:    push   %ebp                      # -
0x080484d1 <+1>:    mov    %esp,%ebp                 #  |- standard prologue
0x080484d3 <+3>:    sub    $0x28,%esp                # -
0x080484d6 <+6>:    mov    0x8(%ebp),%eax            # get argument
0x080484d9 <+9>:    movl   $0x80484f8,-0x14(%ebp)    # set up labels array
0x080484e0 <+16>:   movl   $0x8048510,-0x10(%ebp)
0x080484e7 <+23>:   movl   $0x8048528,-0xc(%ebp)
0x080484ee <+30>:   jmp    *-0x14(%ebp,%eax,4)       # jump to appropriate sect.
0x080484f2 <+34>:   lea    0x0(%esi),%esi
0x08048470 <+0>:    push   %ebp                      # -
0x08048471 <+1>:    mov    %esp,%ebp                 #  |- standard prologue
0x08048473 <+3>:    sub    $0x18,%esp                # -
0x08048476 <+6>:    mov    0x8(%ebp),%eax            # get argument
0x08048479 <+9>:    cmp    $0x1,%eax
0x0804847c <+12>:   je     0x80484b8 <switchFunc+72> # jump here if value == 1
0x0804847e <+14>:   cmp    $0x2,%eax
0x08048481 <+17>:   je     0x80484a0 <switchFunc+48> # if value == 2
0x08048483 <+19>:   test   %eax,%eax
0x08048485 <+21>:   jne    0x804849b <switchFunc+43> # if value != 0 return
0x08048470:推送%ebp#-
0x08048471:mov%esp,%ebp#|-标准开场白
0x08048473:sub$0x18,%esp#-
0x08048476:mov 0x8(%ebp),%eax#get参数
0x08048479:cmp$0x1,%eax
0x0804847c:je 0x80484b8#如果值==1,则跳到此处
0x0804847e:cmp$0x2,%eax
0x08048481:je 0x80484a0#如果值==2
0x08048483:测试%eax,%eax
0x08048485:jne 0x804849b#如果值!=0返回
这两个代码段都有一个“缓慢的部分”:第一个代码段花费大部分时间设置labels数组,而第二个代码段在决定选择哪条路径上很慢。因此,基本上,它们的执行时间几乎相同。

那哪一个更好?第二个是带有
开关的
结构。它是标准的C语言,可读性、可维护性和清晰性更高。

那么为什么要在gcc编译器中添加它呢?我不知道,可能是为了支持这样的情况,即动态选择的标签的地址被存储起来,以便以后在函数中使用。这被称为直接线程,与在性能非常敏感的部分使用switch相比具有显著的优势,由于只发生一个分支预测失误。不过,你是对的,在性能不受关注的情况下,它不应该被用作switch语句的常用替代品。同意@MartinKällman。例如:Erlang/OTP有一个特定的文件,
beam_emu.c
,当编译时没有标签作为值gcc扩展名,使整个Erlang速度降低30-40%。@Martin这在大多数gcc版本中实际上是不正确的-它将代码简化为幕后的switch语句,并返回到频繁的预测失误。因此,对于直接线程来说,它几乎是无用的。@Mr.32:首先,您必须使用包含调试符号的(-gcc上的g开关)进行编译。然后使用一个程序,比如gdb(命令是
disass
,只有汇编代码),或者更好的objdump(
objdump-S-d
,C代码与asm混合)@Mr.32:没问题,玩得开心点:)@BlackBear,@Mr.32,至少在gcc中,有一种更简单的方法可以获得汇编程序:只需使用
-S
而不是
-c
@Jens Gustedt:当然!但是你知道,你总是忘记最简单的事情@失败的黑耳推测性执行,即分支预测器预测错误所采用的路径,仍然会导致管道刷新。推测性执行只有在预测成功时才有用。switch语句是预测和推测执行的最坏情况。(适用于商品x86,IA64的工作原理不同)