【发布时间】:2018-01-08 14:15:44
【问题描述】:
我将一些程序集与一些 c 链接以测试函数调用的成本,使用以下程序集和 c 源代码(分别使用 fasm 和 gcc)
组装:
format ELF
public no_call as "_no_call"
public normal_call as "_normal_call"
section '.text' executable
iter equ 100000000
no_call:
mov ecx, iter
@@:
push ecx
pop ecx
dec ecx
cmp ecx, 0
jne @b
ret
normal_function:
ret
normal_call:
mov ecx, iter
@@:
push ecx
call normal_function
pop ecx
dec ecx
cmp ecx, 0
jne @b
ret
c 来源:
#include <stdio.h>
#include <time.h>
extern int no_call();
extern int normal_call();
int main()
{
clock_t ct1, ct2;
ct1 = clock();
no_call();
ct2 = clock();
printf("\n\n%d\n", ct2 - ct1);
ct1 = clock();
normal_call();
ct2 = clock();
printf("%d\n", ct2 - ct1);
return 0;
}
我得到的结果令人惊讶。首先,速度取决于我链接的顺序很重要。如果我链接为gcc intern.o extern.o,典型的输出是
162
181
但是以相反的顺序链接gcc extern.o intern.o,我得到的输出更像:
162
130
它们的不同非常令人惊讶,但这不是我要问的问题。 (relevant question here)
我要问的问题是,为什么在第二次运行中,有函数调用的循环比没有函数调用的循环快,调用函数的成本显然是负的。
编辑: 只是提一下在 cmets 中尝试过的一些事情:
- 在编译的字节码中,函数调用没有被优化掉。
- 将函数和循环的对齐方式调整为 4 到 64 字节边界的所有内容并没有加快 no_call,尽管某些对齐方式确实减慢了 normal_call 的速度
- 让 CPU/OS 有机会通过多次调用函数而不是仅仅一次来进行预热对测量的时间长度没有明显影响,更改调用顺序或单独运行也没有
- 运行更长时间不会影响比率,例如运行 1000 倍的时间我的运行时间得到了
162.168和131.578秒
另外,在修改汇编代码以对齐字节后,我测试了给函数集一个额外的偏移量,并得出了一些更奇怪的结论。这是更新的代码:
format ELF
public no_call as "_no_call"
public normal_call as "_normal_call"
section '.text' executable
iter equ 100000000
offset equ 23 ; this is the number I am changing
times offset nop
times 16 nop
no_call:
mov ecx, iter
no_call.loop_start:
push ecx
pop ecx
dec ecx
cmp ecx, 0
jne no_call.loop_start
ret
times 55 nop
normal_function:
ret
times 58 nop
normal_call:
mov ecx, iter
normal_call.loop_start:
push ecx
call normal_function
pop ecx
dec ecx
cmp ecx, 0
jne normal_call.loop_start
ret
我不得不手动(且不可移植)强制 64 字节对齐,因为 FASM 不支持可执行部分的超过 4 字节对齐,至少在我的机器上是这样。将程序偏移offset 字节,这是我发现的。
if (20 <= offset mod 128 <= 31) then we get an output of (approximately):
162
131
else
162 (+/- 10)
162 (+/- 10)
完全不知道该怎么做,但这就是我目前发现的
编辑 2:
我注意到的另一件事是,如果从这两个函数中删除 push ecx 和 pop ecx,输出变为
30
125
这表明这是其中最昂贵的部分。堆栈对齐两次都是相同的,所以这不是差异的原因。我最好的猜测是,硬件以某种方式进行了优化,以期待在推送或类似的东西后调用,但我不知道这样的事情
【问题讨论】:
-
@Eugene Sh.你会推荐什么?
-
好吧,虽然我猜
clock很好。尝试查看编译后的 C 代码的生成程序集。此外,看起来(判断链接顺序很重要)正在发生一些链接时间优化。 -
大部分跳转到达的地址(
jne @b的目标)很重要。不幸的是,您没有明确命名它们。no_call和normal_call仅使用一次,因此任何未对齐的惩罚都不重要(远远超出clock计时的 [im] 精度)。并且由于normal_function被广泛调用,因此对齐它也可能有所帮助。通常 4 或 8 个边界就足够了,但可以随意尝试最多 64 个(我认为现代缓存线长 32B?但 64 对任何事情来说都足够了)。 -
导致结果倾斜的另一件事可能是负载下 CPU 频率的动态变化,也许 no-call 循环被理解为 idle-loop 并且 CPU+OS 确实切换了频率。下来,虽然我认为这不太可能在 CPU 中进行如此复杂的代码分析。但是您正在跳过预热阶段,操作系统可能需要一段时间才能检测到 100% 的 CPU 内核使用率,然后才能提高功率,所以可能先对
no_call+normal_call进行一次非时钟运行,两者都可以提高 CPU 频率。并使两种变体的缓存状态相似(预缓存)。 -
@rtpax - 我用 Visual Studio / Windows 尝试了相同的代码。我添加了一个零,更改为
iter equ 1000000000运行时间延长了 10 倍。这两个功能的运行时间约为 1.55 秒。我在循环之前尝试了align 16,但并没有显着不同。整个程序适合代码缓存,这可能是对齐没有帮助的原因。
标签: c performance assembly x86 fasm