我使用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函数做更多的工作 - 它正在初始化mult
,然后进行乘法并将结果赋值给mult
,然后将值从mult
推送到返回位置。
编译器善于优化;在基本算术上你不会轻易击败他们。
如果你真的想要改进,请使用static inline int multiply(int a, int b) { return a * b; }
。或者只是在调用代码而不是a * b
中编写int x = multiply(a, b);
(或等效的)。
这种微基准测试的尝试几乎在所有方面都太天真,无法获得任何有意义的结果。
即使你修复了表面问题(所以代码没有优化掉),在你能得出关于什么时候你的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 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性能,你需要首先理解编译器如何优化。