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

问题描述 投票:-1回答:2

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

我花了他们每个人的执行时间,尽管他们的时间非常接近,但C函数比汇编代码中的函数快一点。

我想知道为什么,因为我期望asm更快。是因为GCC的“asm”关键字的额外“呼叫”(我不知道使用什么词)?

这是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 gcc assembly time inline-assembly
2个回答
3
投票

汇编函数比C函数做更多的工作 - 它正在初始化mult,然后进行乘法并将结果赋值给mult,然后将值从mult推送到返回位置。

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

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


0
投票

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

即使你修复了表面问题(所以代码没有优化掉),在你能得出关于什么时候你的asm*更好的结论之前,还有一些重大的深层问题。

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


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

即使它们不是,单个imul指令基本上是无法测量的,具有与clock()一样多的开销。也许如果你用lfence序列化迫使CPU等待imul退休,在rdtsc之前...参见RDTSCP in NASM always returns the same value

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


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

对于单个x86指令仅测量一个数字并不能告诉您太多。您需要测量延迟,吞吐量和前端uop成本,以正确表征其成本。现代x86 CPU是超标量无序流水线,因此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结果。)

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

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

我能想象的asm帮助的唯一用例是,如果编译器在实际前端绑定的情况下使用2x LEA指令,那么imul $constant, %[src], %[dst]会让它以1 uop而不是2进行复制和乘法。但是你的asm删除了使用immediates的可能性(你只允许注册约束),GNU C inline不能让你使用不同的模板立即与注册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 explorerclang7.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性能,你需要首先理解编译器如何优化。

© www.soinside.com 2019 - 2024. All rights reserved.