为什么 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?
我用 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?