Isk*_*rak 29 c++ math performance gcc
我需要在我的一些代码中使用对数函数,但基数并不重要.于是我开始挑之间log(),log2()和log10()通过性能,提供我发现任何显著差异.(我将参照所述功能ln,lb和lg分别地).
为什么我这样烦恼?因为每次迭代的优化算法我会经常调用该函数400,000,000次.这既不是可选的,也不是我的问题的主题.
我设置了一些非常基本的测试,如下所示:
timespec start, end;
double sum = 0, m;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
for (int n = 1; n < INT_MAX; ++n)
{
m = n * 10.1;
sum += log(m);
}
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
cout << "ln=";
cout << diff(start, end).tv_sec << ":" << diff(start, end).tv_nsec << endl;
... // likewise for log2 and log10
Run Code Online (Sandbox Code Playgroud)
(timespec diff(timespec start,timespec end)如果你愿意......)
获得了以下结果:
GCC v4.6.3
-O0
ln=140:516853107
lb=155:878100147
lg=173:534086352
-O1
ln=133:948317112
lb=144:78885393
lg=163:870021712
-O2
ln=9:108117039
lb=9:134447209
lg=4:87951676
-O3
ln=9:102016996
lb=9:204672042
lg=4:153153558
Run Code Online (Sandbox Code Playgroud)
我已经查看了编译的输出-S,但是我真的没有足够的抓握装配器来完全理解差异.-S输出:-O0-S,-O3 -S
为什么lg用O2/O3更好地优化?
编辑:源代码,注意第三个循环中的拼写错误,这是log10看起来更快的原因(mult.优化了).我接受了我认为最接近的答案,因为这个问题现在已经结束了,尽管我从drhirsch和janneb的答案中学到了很多东西.
这将取决于C库中的log()函数的实现,编译器版本,硬件架构等.无论如何,下面我在x86-64上使用GCC 4.4和glibc 2.11.
更改示例以便我添加一行
cout << "sum=" << sum << endl;
Run Code Online (Sandbox Code Playgroud)
这会阻止编译器优化掉log()调用,正如我在评论中提到的,我得到以下时间(仅限整秒,-O2):
这些时间似乎与原始帖子中的-O0和-O1时间大致一致; 在更高的优化级别,日志评估被优化掉,因此-O2和-O3结果是如此不同.
此外,使用"perf"分析器查看日志示例,报告中的前5名违规者是
# Samples: 3259205
#
# Overhead Command Shared Object Symbol
# ........ .............. ......................... ......
#
87.96% log /lib/libm-2.11.1.so [.] __ieee754_log
5.51% log /lib/libm-2.11.1.so [.] __log
2.88% log ./log [.] main
2.84% log /lib/libm-2.11.1.so [.] __isnan
0.69% log ./log [.] log@plt
Run Code Online (Sandbox Code Playgroud)
除main之外,所有其他符号都与log()调用相关.总结这些,我们可以得出结论,此示例的总运行时间的97%用于log().
可以在glibc git repo中找到__ieee754_log的实现.相应地,其他实现是:log2,log10.请注意,之前的链接是针对HEAD版本的,对于已发布的版本,请参阅其相应的分支
我注意到一些事情。如果我编译(GCC 4.5.3)你的汇编器列表-O3 -S,g++ logflt.S -lrt我可以重现该行为。我的时间安排是:
ln=6:984160044
lb=6:950842852
lg=3:64288522
Run Code Online (Sandbox Code Playgroud)
然后我检查了输出objdump -SC a.out。与查看.S文件相比,我更喜欢这样做,因为有些结构我(尚)不理解。代码不太容易阅读,但我发现以下内容:
在调用之前log或log2使用转换参数之前
400900: f2 0f 2a c3 cvtsi2sd %ebx,%xmm0
400904: 66 0f 57 c9 xorpd %xmm1,%xmm1
400908: f2 0f 59 05 60 04 00 mulsd 0x460(%rip),%xmm0
40090f: 00
400910: 66 0f 2e c8 ucomisd %xmm0,%xmm1
Run Code Online (Sandbox Code Playgroud)
0x460(%rip)是指向十六进制值的相对地址0000 00000000 33333333 33332440。这是一个 16 字节的 SSEdouble对,其中只有一个 double 很重要(代码使用标量 SSE)。这双是10.1。mulsd从而在 C++ 行中执行乘法m = n * 10.1;。
log10是不同的:
400a40: f2 0f 2a c3 cvtsi2sd %ebx,%xmm0
400a44: 66 0f 57 c9 xorpd %xmm1,%xmm1
400a48: 66 0f 2e c8 ucomisd %xmm0,%xmm1
Run Code Online (Sandbox Code Playgroud)
log10我想对于你忘记执行乘法的情况!所以你只是log10一次又一次地用相同的值调用...如果CPU足够聪明来优化它,我不会感到惊讶。
编辑:我现在非常确定这就是问题所在,因为在您的其他列表中(-O0 -S)乘法被正确执行 - 所以请发布您的代码并让其他人证明我错了!
EDIT2:GCC 摆脱这种乘法的一种方法是使用以下身份:
log(n * 10.1) = log(n) + log(10.1)
Run Code Online (Sandbox Code Playgroud)
但在这种情况下log(10.1),必须计算一次,而且我没有看到这个代码。我也怀疑 GCC 会这样做,log10但不会为log和log2。
不幸的是,OP未能向我们展示原始代码,他选择将代码混淆,稍微将其转换为汇编.
在汇编代码中OP链接(由我注释):
.L10:
cvtsi2sd %ebx, %xmm0 // convert i to double
xorpd %xmm1, %xmm1 // zero
mulsd .LC0(%rip), %xmm0 // multiply i with 10.1
ucomisd %xmm0, %xmm1 // compare with zero
jae .L31 // always below, never jump
addl $1, %ebx // i++
cmpl $2147483647, %ebx // end of for loop
jne .L10
...
.L31:
call log10, log2, whatever... // this point is never reached
Run Code Online (Sandbox Code Playgroud)
可以看到调用log永远不会被执行,特别是如果你使用gdb单步执行它.所有代码都是2 31次乘法和双精度的比较.
这也解释了编译时日志函数的执行速度惊人地增加了30倍-O2,以防任何人发现这也很奇怪.
编辑:
for (int n = 1; n < INT_MAX; ++n)
{
m = n * 10.1;
sum += log(m);
}
Run Code Online (Sandbox Code Playgroud)
编译器无法完全优化循环,因为她无法证明调用log将始终成功 - 如果参数为负,则会产生副作用.因此,她通过与零的比较来替换循环 - log只有在乘法的结果小于或小于零时才执行.这意味着它永远不会被执行:-)
停留在循环中的是乘法和测试,如果结果可能是负的.
如果我添加-ffast-math到编译器选项中,会发生一个有趣的结果,这会使编译器免于严格的IEEE兼容性:
ln=0:000000944
lb=0:000000475
lg=0:000000357
Run Code Online (Sandbox Code Playgroud)