我需要在我的一些代码中使用对数函数,但基数并不重要。所以我开始在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
(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
我已经查看了使用-S
进行编译的输出,但是我对于汇编程序没有足够的掌握来完全理解这些差异。 -S
输出:-O0 -S,-O3 -S
为什么lg
能更好地优化O2 / O3?
编辑:Source code,注意第三个循环中的拼写错误,这是log10看起来更快的原因(mult。优化了)。我接受了我认为最接近的答案,因为这个问题现在已经结束,尽管我从drhirsch和janneb的答案中学到了很多。
答案 0 :(得分:6)
这将取决于C库中的log()函数的实现,编译器版本,硬件架构等。无论如何,下面我在x86-64上使用GCC 4.4和glibc 2.11。
更改示例以便我添加一行
cout << "sum=" << sum << endl;
阻止编译器优化掉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
除main之外,所有其他符号都与log()调用相关。总结这些,我们可以得出结论,此示例的总运行时间的97%用于log()。
可以找到__ieee754_log的实现here in the glibc git repo。相应地,其他实现是:log2,log10。请注意,以前的链接是HEAD版本,发布版本请参见相应的分支
答案 1 :(得分:5)
不幸的是,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
可以看到永远不会执行对log
的调用,特别是如果您使用gdb单步调用它。所有代码都是2 31 乘法和双重比较。
这也解释了使用-O2
编译时日志函数的执行速度惊人增加了30倍,以防有人发现这种情况。
编辑:
for (int n = 1; n < INT_MAX; ++n)
{
m = n * 10.1;
sum += log(m);
}
编译器无法完全优化循环,因为她无法证明对log
的调用将始终成功 - 如果参数为负,则会产生副作用。所以她用零比较替换循环 - 如果乘法的结果小于或小于零,则仅执行log
。这意味着它永远不会执行:-)
如果结果可能是负数,那么停留在循环中的是乘法和测试。
如果我将-ffast-math
添加到编译器选项中会产生一个有趣的结果,这会使编译器免受严格的IEEE合规性的影响:
ln=0:000000944
lb=0:000000475
lg=0:000000357
答案 2 :(得分:4)
我注意到了一些事情。如果我编译(GCC 4.5.3)您的汇编程序列表-O3 -S
与g++ logflt.S -lrt
我可以重现该行为。我的时间是:
ln=6:984160044
lb=6:950842852
lg=3:64288522
然后我用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
0x460(%rip)
是一个相对地址,指向十六进制值0000 00000000 33333333 33332440
。这是一个16字节的SSE 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
我认为log10
的情况你忘了执行乘法!所以你只是一次又一次地调用log10
相同的值......我如果cpu足够聪明以优化它,我不会感到惊讶。
编辑:我现在非常确定这是问题所在,因为在您的其他商家信息中-O0 -S
)正确执行了倍增 - 所以请发布您的代码并让其他人证明我错了!
EDIT2:GCC 可以摆脱这种乘法的一种方法是使用以下标识:
log(n * 10.1) = log(n) + log(10.1)
但在这种情况下log(10.1)
必须计算一次,我不会看到这个代码。我也怀疑GCC会为log10
而不是log
和log2
执行此操作。
答案 3 :(得分:3)
你正在接近这个问题并且正在得出结论。
使用时钟不足以进行性能分析。使用合适的分析器而不是时钟(gprof或AQTime7)。 Profiler必须能够提供每行时序。您的问题是您认为瓶颈在于日志功能。然而,int-to-float转换并不是很快,也可能是瓶颈。另一件事是gcc附带了你可以阅读的源代码。
现在,假设瓶颈实际上位于日志功能中:
正如你应该知道的那样,双打精度有限 - 只有15..17十进制数。这意味着对于较大的对数基数,当您达到精度限制时,您很快就会达到这种情况。
即。 10^(log10(2^32) + 10^-15) - 2^32
== 9.8895 * 10^-6
,但2^(log2(2^32) + 10^-15) - 2^32
== 2.977 * 10^-6
和100^(log100(2^32) + 10^-15) - 2^32
== 0.00001977
,log2(INT_MAX) > log10(INT_MAX)
这意味着更大的对数基数,如果对数函数试图“搜索”一个正确的结果,它将很快达到由于四舍五入的错误而不再可能修改预测结果的情况。但是,这仍然是一个猜测。
还有其他计算对数的方法。例如,log10(x) == ln(x)/ln(10)
如果对数函数以这种方式计算它,你会得到几乎相似的时间。
我的建议是(停止浪费时间),用时钟功能以外的方式描述你的程序(重新发明轮子是个坏主意,不使用现有的分析工具重新发明轮子,加上一个好的分析器将能够从日志函数中提供每行时序),读取日志函数的gcc源代码(毕竟它可用)和汇编输出。如果您不了解汇编输出,那么这将是学习如何阅读它的好机会。
如果拥有更快的对数函数真的很重要,并且算法优化真的不可能(如果对数确实是瓶颈,你可以缓存结果,例如)你可以尝试找到更快的算法实现,但如果我在这种情况下,你是否只是试图通过并行化任务来解决问题 - 例如。