为什么 C 中的 asm 代码函数比 c 代码函数花费更多时间?

Why does the asm code function in C, takes more time than the c code function?

我用 C 写了一个简单的乘法函数,用汇编代码写了另一个,使用 GCC 的 "asm" 关键字。

我计算了他们每个人的执行时间,虽然他们的时间很接近,但 C 函数比汇编代码中的函数快一点。

我想知道为什么,因为我希望 asm 更快。是因为 GCC 的 "asm" 关键字多了 "call"(我不知道该用什么词)?

这是 C 函数:

int multiply (int a, int b){return a*b;}

这是 C 文件中的 asm

int asmMultiply(int a, int b){  
    asm ("imull %1,%0;"
             : "+r" (a)           
             : "r" (b)
    );
    return a;
}

我的主要时间我在哪里:

int main(){
   int n = 50000;
   clock_t asmClock = clock();
   while(n>0){
       asmMultiply(4,5);
       n--;
    }

   asmClock = clock() - asmClock;  
   double asmTime = ((double)asmClock)/CLOCKS_PER_SEC; 

   clock_t cClock = clock();
   n = 50000;
   while(n>0){
       multiply(4,5);
       n--;
   }
   cClock = clock() - cClock;  
   double cTime = ((double)cClock)/CLOCKS_PER_SEC;  

  printf("Asm time: %f\n",asmTime);
  printf("C code time: %f\n",cTime);

谢谢!

汇编函数比 C 函数做更多的工作 — 它初始化 mult,然后进行乘法并将结果赋给 mult,然后从 mult 中压入值] 进入 return 位置。

编译器擅长优化;你不会轻易在基本算术上击败他们。

如果你真的想要改进,使用static inline int multiply(int a, int b) { return a * b; }。或者只在调用代码中写 a * b (或等效的)而不是 int x = multiply(a, b);.

这种微基准测试尝试在几乎所有可能的方面都太天真了,您无法获得任何有意义的结果。

即使您解决了表面问题(因此代码没有优化掉),在您得出关于什么时候您的 asm* 更好的任何结论之前,仍然存在重大的深层问题.

(提示:可能永远不会。编译器已经知道如何最佳地乘以整数,并理解该操作的语义。强制它使用 imul 而不是 auto -矢量化或进行其他优化将是一种损失。)


两个定时区域都是空的,因为两个乘法都可以优化掉。 (asm 不是 asm volatile,您不使用结果。)您只测量噪声 and/or CPU 频率上升在 clock() 开销之前达到最大涡轮增压。

即使它们不是,单个 imul 指令基本上无法用开销与 clock() 一样多的函数来衡量。也许如果您使用 lfence 进行序列化以强制 CPU 等待 imul 退出,然后再 rdtsc... 请参阅

或者您在编译时禁用了优化,这是没有意义的。


如果没有某种涉及循环的上下文,您基本上无法衡量 C * 运算符与内联汇编的对比。然后它将 对于该上下文 ,取决于您使用内联 asm 击败了哪些优化。 (如果你做了什么来阻止编译器为纯 C 版本优化工作呢。)

仅测量一条 x86 指令的一个数字并不能告诉您太多信息。您需要测量延迟、吞吐量和前端 uop 成本以正确表征其成本。现代 x86 CPUs 是超标量乱序流水线,因此 2 条指令的成本总和取决于它们是否相互依赖以及其他周围环境。 How many CPU cycles are needed for each assembly instruction?


函数的独立定义是相同的,在您更改为让编译器选择寄存器之后,您的 asm 可以 内联一些效率,但它仍然是优化失败的. gcc 在编译时知道 5*4 = 20,因此如果您确实使用结果 multiply(4,5) 可以优化为立即数 20。但是 gcc 不知道 asm 做了什么,所以它只需要至少输入一次。 (非 volatile 意味着如果您在循环中使用 asmMultiply(4,5) 它可以对结果进行 CSE。)

因此,除其他外,内联汇编击败了持续传播。即使只有一个输入是常量,而另一个是运行时变量,这也很重要。许多小型整数乘法器可以用一个或两个 LEA 指令或一个移位来实现(对于现代 x86 上的 imul,延迟低于 3c)。

https://gcc.gnu.org/wiki/DontUseInlineAsm

我能想象的唯一用例 asm 帮助是如果编译器在实际上是前端绑定的情况下使用 2x LEA 指令,其中 imul $constant, %[src], %[dst] 会让它复制并-乘以 1 uop 而不是 2。但是您的 asm 消除了使用立即数的可能性(您只允许寄存器约束),并且 GNU C 内联不能让您对立即数与寄存器 arg 使用不同的模板。也许如果您对仅寄存器部分使用了多种替代约束和匹配的寄存器约束?但是不,你仍然必须有类似 asm("%2, %1, %0" :...) 的东西,这对 reg,reg.

不起作用

您可以使用 if(__builtin_constant_p(a)) { asm using imul-immediate } else { return a*b; },它将与 GCC 一起使用,让您击败 LEA。或者无论如何只需要一个常量乘数,因为您只想将它​​用于特定的 gcc 版本来解决特定的未优化问题。 (即它是如此的小众以至于在实践中你不会这样做。


您的代码 on the Godbolt compiler explorer,其中 clang7.0 -O3 用于 x86-64 System V 调用约定:

# clang7.0 -O3   (The functions both inline and optimize away)
main:                                   # @main
    push    rbx
    sub     rsp, 16
    call    clock
    mov     rbx, rax                 # save the return value
    call    clock
    sub     rax, rbx                 # end - start time
    cvtsi2sd        xmm0, rax
    divsd   xmm0, qword ptr [rip + .LCPI2_0]
    movsd   qword ptr [rsp + 8], xmm0 # 8-byte Spill


    call    clock
    mov     rbx, rax
    call    clock
    sub     rax, rbx             # same block again for the 2nd group.

    xorps   xmm0, xmm0
    cvtsi2sd        xmm0, rax
    divsd   xmm0, qword ptr [rip + .LCPI2_0]
    movsd   qword ptr [rsp], xmm0   # 8-byte Spill
    mov     edi, offset .L.str
    mov     al, 1
    movsd   xmm0, qword ptr [rsp + 8] # 8-byte Reload
    call    printf
    mov     edi, offset .L.str.1
    mov     al, 1
    movsd   xmm0, qword ptr [rsp]   # 8-byte Reload
    call    printf
    xor     eax, eax
    add     rsp, 16
    pop     rbx
    ret

TL:DR:如果您想了解这种细粒度级别的内联 asm 性能,您需要首先了解编译器如何优化。

  • Modern x86 cost model

  • What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?