【问题标题】:Why does the asm code function in C, takes more time than the c code function?为什么 C 中的 asm 代码函数比 c 代码函数花费更多时间?
【发布时间】:2019-07-17 20:44:36
【问题描述】:

我使用 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);

谢谢!

【问题讨论】:

  • 表达你如何测量程序中经过的时间。
  • “是不是因为对 GCC 的“asm”关键字有额外的“调用”(我不知道该用什么词)?” - 不,是因为你的 asm 很慢。不要在琐碎的代码上与编译器竞争,他们会在 99% 的情况下用完美的机器代码击败你(通常优化得非常好,它可能会让你感到困惑并且看起来很慢 - 如果你的机器知识是不是所需的专业知识,并且您可能对现代 x86 的工作方式有一些幼稚的假设)。提升你的机器知识(假设你的问题措辞和内容是初学者,达到“专家”或“大师”)并使用一些中等复杂的 C 源代码 = 你可以赢。
  • @Ped7g:使用asm("imul %1, %0" : "+r"(a) : "rme"(b) ); 为编译器提供寄存器、内存或立即数的选择@ GCC 擅长于此,但如果可以选择,clang 通常会选择内存,即使这意味着溢出register var first :/ 但是不,如果任一输入是一个常数,可以用 LEA 或 shift 来完成,例如 95,或者2. 或作为 LEA 的一部分折叠成一个附加组件。 (但可能类似肯定)。 gcc.gnu.org/wiki/DontUseInlineAsm
  • 您一直在使用不同的基准测试代码编辑您的问题,但您仍然没有显示任何实际时间结果、编译器版本/选项或硬件信息。 C 不是凭空存在的,编译器版本/选项和硬件都很重要。您更新的代码仍然无法阻止 multiply() 完全优化。见CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!"

标签: c gcc assembly time inline-assembly


【解决方案1】:

这种微基准测试的尝试在几乎所有可能的方面都过于幼稚,以至于您无法获得任何有意义的结果。

即使您修复了表面问题(因此代码没有优化掉),在您得出任何关于您的 asm 何时优于 * 的结论之前,仍存在重大的深层问题。

(提示:可能永远不会。编译器已经知道如何优化整数相乘,并理解该操作的语义。强制它使用imul 而不是自动矢量化或进行其他优化是会吃亏的。)


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

即使它们不是,单个imul 指令对于具有与clock() 一样多的开销的函数基本上是无法测量的。也许如果您使用lfence 进行序列化以强制CPU 在rdtsc 之前等待imul 退出...请参阅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 击败了持续传播。即使只有一个输入是常量,而另一个是运行时变量,这也很重要。许多小整数乘法器可以使用 1 个或 2 个 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 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:如果您想了解这种细粒度级别的内联汇编性能,您首先需要了解编译器如何优化。

【讨论】:

    【解决方案2】:

    汇编函数比 C 函数做的工作更多——它初始化mult,然后进行乘法运算并将结果分配给mult,然后将mult 中的值推入返回位置。

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

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

    【讨论】:

    • 正如mult 的约束所说=,编译器实际上知道零被丢弃而没有使用,因此它将删除mult=0; 分配。它失败的地方是对源寄存器的严格要求和简单的imul 指令的整体“黑盒”,使得优化器无法弄清楚发生了什么,而它知道如何为琐碎构建“最佳”机器代码乘法非常好(将其与更复杂的计算/源放在一起,结果将迅速从“最佳”水平转移到“非常好”水平,如果足够努力,会给人类一些机会)
    • 请注意,OP 选择ebx 作为源操作数,要求编译器在使用前保存它。
    猜你喜欢
    • 1970-01-01
    • 2012-09-19
    • 2017-07-06
    • 1970-01-01
    • 2021-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-20
    相关资源
    最近更新 更多