C 为什么调用JMP就足够了?

C 为什么调用JMP就足够了?,c,gcc,assembly,x86,C,Gcc,Assembly,X86,我有两个文件: #include <stdio.h> static inline void print0() { printf("Zero"); } static inline void print1() { printf("One"); } static inline void print2() { printf("Two"); } static inline void print3() { printf("Three"); } static inline void print4

我有两个文件:

#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

int main()
{
    unsigned int input;
    scanf("%u", &input);

    switch (input)
    {
        case 0: print0(); break;
        case 1: print1(); break;
        case 2: print2(); break;
        case 3: print3(); break;
        case 4: print4(); break;
    }
    return 0;
}
#包括
静态内联void print0(){printf(“零”);}
静态内联void print1(){printf(“一”);}
静态内联void print2(){printf(“Two”);}
静态内联void print3(){printf(“三”);}
静态内联void print4(){printf(“四”);}
int main()
{
无符号整数输入;
scanf(“%u”,&input);
开关(输入)
{
案例0:print0();中断;
案例1:print1();中断;
案例2:print2();中断;
案例3:print3();中断;
案例4:print4();中断;
}
返回0;
}

#包括
静态内联void print0(){printf(“零”);}
静态内联void print1(){printf(“一”);}
静态内联void print2(){printf(“Two”);}
静态内联void print3(){printf(“三”);}
静态内联void print4(){printf(“四”);}
int main()
{
无符号整数输入;
scanf(“%u”,&input);
静态无效(*jt[])()={print0,print1,print2,print3,print4};
jt[input]();
返回0;
}
我希望它们被编译成几乎相同的汇编代码。在这两种情况下都会生成跳转表,但中的调用由
jmp
表示,而中的调用由
调用
表示。为什么编译器不优化
调用
s?是否可以提示gcc我希望看到
jmp
s而不是
call
s

使用gcc-Wall-Winline-O3-S-masm=intel编译,gcc版本4.6.2。GCC4.8.0生成的代码稍少,但问题仍然存在


UPD:将
jt
定义为
const void(*const jt[])()={print0,print1,print2,print3,print4}并使函数
静态常量内联
没有帮助:

我的猜测是,这种优化与这样一个事实有关,即在
开关后立即有一个
return
语句:优化器意识到它可以利用嵌入到
print0
print4
函数,并将
调用减少到
jmp
;CPU在所选的
printN
中点击的
ret
作为
的返回

尝试在开关后插入一些代码,看看编译器是否将
jmp
替换为
call

#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

int main()
{
    unsigned int input;
    scanf("%u", &input);

    switch (input)
    {
        case 0: print0(); break;
        case 1: print1(); break;
        case 2: print2(); break;
        case 3: print3(); break;
        case 4: print4(); break;
    }
    /* Inserting this line should force the compiler to use call */
    printf("\nDone");
    return 0;
}

因为函数指针数组是可变的。编译器已经决定不能假定指针不会被更改。您可能会发现程序集不同于C++,和/或使JT const。

< p>编译器编写者有很多工作要做。显然,他们会优先考虑回报最大、最快的工作

Switch语句在所有类型的代码中都很常见,因此对它们执行的任何优化都会对许多程序产生影响

此代码

jt[input](); 

不太常见,因此在编译器设计人员的待办事项列表中要长得多。也许他们还没有(还)发现值得努力去优化它?这会为他们赢得任何已知的基准吗?或者改进一些广泛使用的代码库?

您分析过不同的代码吗?我认为可能有人认为间接调用是优化的。以下分析是针对x64平台(MinGW)的GCC 4.6.1进行的

如果查看使用
jt[input]()
时发生的情况,调用将导致执行以下代码序列:

  • printX()
    函数之一的间接调用
  • 函数为
    printf()
    设置参数,然后
  • 跳转到
    printf()
  • printf()
    调用将直接返回到`间接调用'的站点
总共有3个分支机构

使用switch语句时会发生以下情况:

  • 对于每种情况(内联
    printX()
    calls),间接跳转到一位自定义代码
  • “case handler”为
    printf()
    调用加载适当的参数
  • 调用
    printf()
  • printf()
    调用将返回到
  • 跳转到开关的退出点(除了一个内联退出代码的案例处理程序-其他案例跳转到那里)
总共4个分支(一般情况下)

在这两种情况下,您都有: -间接分支(一个是呼叫,另一个是跳转) -到
printf()
的分支(一个是跳转,另一个是调用) -返回呼叫站点的分支

但是,当使用
switch
语句时,会有一个额外的分支到达开关的“末端”(在大多数情况下)

现在,如果你真的分析了一些东西,处理器处理间接跳转的速度可能比间接调用快,但我猜即使是这样,基于开关的代码中使用的额外分支仍然会通过函数指针推动有利于调用的尺度


对于那些感兴趣的人,这里是使用
jk[input]()生成的汇编程序(使用GCC MinGW 4.6.1针对x64编译的两个示例,使用的选项是
-Wall-Winline-O3-S-masm=intel
):

下面是为基于交换机的实现生成的代码:

main:
    sub rsp, 56
    .seh_stackalloc 56
    .seh_endprologue
    call    __main
    lea rdx, 44[rsp]
    lea rcx, .LC0[rip]
    call    scanf
    cmp DWORD PTR 44[rsp], 4
    ja  .L2
    mov edx, DWORD PTR 44[rsp]
    lea rax, .L8[rip]
    movsx   rdx, DWORD PTR [rax+rdx*4]
    add rax, rdx
    jmp rax
    .section .rdata,"dr"
    .align 4
.L8:
    .long   .L3-.L8
    .long   .L4-.L8
    .long   .L5-.L8
    .long   .L6-.L8
    .long   .L7-.L8
    .section    .text.startup,"x"
.L7:
    lea rcx, .LC5[rip]
    call    printf
    .p2align 4,,10


.L2:
    xor eax, eax
    add rsp, 56
    ret

.L6:
    lea rcx, .LC4[rip]
    call    printf
    jmp .L2

     // all the other cases are essentially the same as the one above (.L6)
     // where they jump to .L2 to exit instead of simply falling through to it
     // like .L7 does

后一个函数的代码是否在间接
调用
和后续
ret
之间不起作用?如果间接调用的地址计算使用的寄存器的值需要后一个函数保存(这意味着它必须在计算之前保存该值,并在计算之后恢复该值),我不会感到惊讶。虽然可以在间接调用之前移动寄存器还原代码,但编译器只能在已编程为识别为合法机会的情况下执行此类代码移动

另外,虽然我认为这不重要,但我还是建议
jt[input](); 
print0:
    .seh_endprologue
    lea rcx, .LC4[rip]
    jmp printf
    .seh_endproc

// similar code is generated for each printX() function
// ...

main:
    sub rsp, 56
    .seh_stackalloc 56
    .seh_endprologue
    call    __main
    lea rdx, 44[rsp]
    lea rcx, .LC5[rip]
    call    scanf
    mov edx, DWORD PTR 44[rsp]
    lea rax, jt.2423[rip]
    call    [QWORD PTR [rax+rdx*8]]
    xor eax, eax
    add rsp, 56
    ret
main:
    sub rsp, 56
    .seh_stackalloc 56
    .seh_endprologue
    call    __main
    lea rdx, 44[rsp]
    lea rcx, .LC0[rip]
    call    scanf
    cmp DWORD PTR 44[rsp], 4
    ja  .L2
    mov edx, DWORD PTR 44[rsp]
    lea rax, .L8[rip]
    movsx   rdx, DWORD PTR [rax+rdx*4]
    add rax, rdx
    jmp rax
    .section .rdata,"dr"
    .align 4
.L8:
    .long   .L3-.L8
    .long   .L4-.L8
    .long   .L5-.L8
    .long   .L6-.L8
    .long   .L7-.L8
    .section    .text.startup,"x"
.L7:
    lea rcx, .LC5[rip]
    call    printf
    .p2align 4,,10


.L2:
    xor eax, eax
    add rsp, 56
    ret

.L6:
    lea rcx, .LC4[rip]
    call    printf
    jmp .L2

     // all the other cases are essentially the same as the one above (.L6)
     // where they jump to .L2 to exit instead of simply falling through to it
     // like .L7 does
  400570:       ff 24 c5 b8 06 40 00    jmpq   *0x4006b8(,%rax,8)
[ ... ]
  400580:       31 c0                   xor    %eax,%eax
  400582:       e8 e1 fe ff ff          callq  400468 <printf@plt>
  400587:       31 c0                   xor    %eax,%eax
  400589:       48 83 c4 08             add    $0x8,%rsp
  40058d:       c3                      retq
  40058e:       bf a4 06 40 00          mov    $0x4006a4,%edi
  400593:       eb eb                   jmp    400580 <main+0x30>
  400595:       bf a9 06 40 00          mov    $0x4006a9,%edi
  40059a:       eb e4                   jmp    400580 <main+0x30>
  40059c:       bf ad 06 40 00          mov    $0x4006ad,%edi
  4005a1:       eb dd                   jmp    400580 <main+0x30>
  4005a3:       bf b1 06 40 00          mov    $0x4006b1,%edi
  4005a8:       eb d6                   jmp    400580 <main+0x30>
[ ... ]
Contents of section .rodata:
[ ... ]
 4006b8 8e054000 p ... ]
jmp <to location that sets arg for printf()>
...
jmp <back to common location for the printf() invocation>
...
call <printf>
...
retq
0000000000400550 <print0>:
[ ... ]
0000000000400560 <print1>:
[ ... ]
0000000000400570 <print2>:
[ ... ]
0000000000400580 <print3>:
[ ... ]
0000000000400590 <print4>:
[ ... ]
00000000004005a0 <main>:
  4005a0:       48 83 ec 08             sub    $0x8,%rsp
  4005a4:       bf d4 06 40 00          mov    $0x4006d4,%edi
  4005a9:       31 c0                   xor    %eax,%eax
  4005ab:       48 8d 74 24 04          lea    0x4(%rsp),%rsi
  4005b0:       e8 c3 fe ff ff          callq  400478 <scanf@plt>
  4005b5:       8b 54 24 04             mov    0x4(%rsp),%edx
  4005b9:       31 c0                   xor    %eax,%eax
  4005bb:       ff 14 d5 60 0a 50 00    callq  *0x500a60(,%rdx,8)
  4005c2:       31 c0                   xor    %eax,%eax
  4005c4:       48 83 c4 08             add    $0x8,%rsp
  4005c8:       c3                      retq
[ ... ]
 500a60 50054000 00000000 60054000 00000000  P.@.....`.@.....
 500a70 70054000 00000000 80054000 00000000  p.@.......@.....
 500a80 90054000 00000000                    ..@.....
#include <stdio.h>

static inline void print0() { printf("Zero"); }
static inline void print1() { printf("One"); }
static inline void print2() { printf("Two"); }
static inline void print3() { printf("Three"); }
static inline void print4() { printf("Four"); }

void main(int argc, char **argv)
{
    static void (*jt[])() = { print0, print1, print2, print3, print4 };
    return jt[argc]();
}
0000000000400550 <main>:
  400550:       48 63 ff                movslq %edi,%rdi
  400553:       31 c0                   xor    %eax,%eax
  400555:       4c 8b 1c fd e0 09 50    mov    0x5009e0(,%rdi,8),%r11
  40055c:       00
  40055d:       41 ff e3                jmpq   *%r11d
0000000000400500 <main>:
  400500:       83 ff 04                cmp    $0x4,%edi
  400503:       77 0b                   ja     400510 <main+0x10>
  400505:       89 f8                   mov    %edi,%eax
  400507:       ff 24 c5 58 06 40 00    jmpq   *0x400658(,%rax,8)
  40050e:       66                      data16
  40050f:       90                      nop
  400510:       f3 c3                   repz retq
  400512:       bf 3c 06 40 00          mov    $0x40063c,%edi
  400517:       31 c0                   xor    %eax,%eax
  400519:       e9 0a ff ff ff          jmpq   400428 <printf@plt>
  40051e:       bf 41 06 40 00          mov    $0x400641,%edi
  400523:       31 c0                   xor    %eax,%eax
  400525:       e9 fe fe ff ff          jmpq   400428 <printf@plt>
  40052a:       bf 46 06 40 00          mov    $0x400646,%edi
  40052f:       31 c0                   xor    %eax,%eax
  400531:       e9 f2 fe ff ff          jmpq   400428 <printf@plt>
  400536:       bf 4a 06 40 00          mov    $0x40064a,%edi
  40053b:       31 c0                   xor    %eax,%eax
  40053d:       e9 e6 fe ff ff          jmpq   400428 <printf@plt>
  400542:       bf 4e 06 40 00          mov    $0x40064e,%edi
  400547:       31 c0                   xor    %eax,%eax
  400549:       e9 da fe ff ff          jmpq   400428 <printf@plt>
  40054e:       90                      nop
  40054f:       90                      nop