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

时间:2019-02-24 06:32:10

标签: c gcc assembly time inline-assembly

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

我花了他们每个人的执行时间,尽管他们的时间非常接近,但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);

谢谢!

2 个答案:

答案 0 :(得分:3)

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

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

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

答案 1 :(得分:0)

这种微基准测试在几乎所有方面都太幼稚了,您无法获得有意义的结果。

即使您解决了表面问题(因此代码也没有得到优化),在得出任何有关asm何时会比*更好的结论之前,仍然存在一些重大的深层问题。 / p>

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


两个定时区域均为空,因为两个乘法都可以优化。 (asm不是asm volatile,并且您不使用结果。)您仅在测量噪声和/或将CPU频率提升到{{1 }}。

即使不是,clock()指令基本上也无法用imul这样的开销来测量。也许如果您在clock()之前用lfence进行序列化以强制CPU等待imul退出,请参阅RDTSCP in NASM always returns the same value

或者您在禁用优化的情况下进行编译,这毫无意义。


在没有某种涉及循环的上下文的情况下,您基本上无法测量C rdtsc和内联汇编程序。然后,将是针对该上下文,具体取决于您通过使用嵌入式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)。但是gcc不知道asm的作用,因此它至少必须将输入一次提供给它。 (不过,非20表示如果您在循环中使用volatile则可以对结果进行CSE。)

因此,内联asm击败了持续传播。即使只有一个输入是常量,而另一个是运行时变量,这也很重要。许多小的整数乘法器可以用一个或2条LEA指令或一个移位(与现代x86上asmMultiply(4,5)的3c相比,延迟较低)来实现。

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

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

您可以使用asm("%2, %1, %0" :...),它将与GCC配合使用以击败LEA。还是无论如何都需要一个常数乘数,因为您只想将其用于特定的gcc版本,以解决特定的遗漏优化问题。 (就是这么小巧,实际上您不会这样做。


您的代码on the Godbolt compiler explorer ,其中if(__builtin_constant_p(a)) { asm using imul-immediate } else { return a*b; }用于x86-64 System V调用约定:

clang7.0 -O3

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