需要解释:log10比log和log2快,但只有O2和更高

时间:2012-05-30 04:45:24

标签: c++ performance math gcc

我需要在我的一些代码中使用对数函数,但基数并不重要。所以我开始在log()log2()log10()之间选择性能,只要我发现有任何显着差异。 (我将分别称为lnlblg

为什么我要为此烦恼?因为每次迭代的优化算法我会经常调用该函数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的答案中学到了很多。

4 个答案:

答案 0 :(得分:6)

这将取决于C库中的log()函数的实现,编译器版本,硬件架构等。无论如何,下面我在x86-64上使用GCC 4.4和glibc 2.11。

更改示例以便我添加一行

cout << "sum=" << sum << endl;

阻止编译器优化掉log()调用,正如我在评论中提到的,我得到以下时间(仅限整秒,-O2):

  • log:98s
  • log2:105s
  • log10:120s

这些时间似乎与原帖中的-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。相应地,其他实现是:log2log10。请注意,以前的链接是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 -Sg++ logflt.S -lrt我可以重现该行为。我的时间是:

ln=6:984160044
lb=6:950842852
lg=3:64288522

然后我用objdump -SC a.out检查输出。我更喜欢这个查看.S文件,因为有一些我还没有理解的结构。代码不是很容易阅读,但我发现以下内容:

在调用loglog2之前,使用

转换参数
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.1mulsd因此在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而不是loglog2执行此操作。

答案 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^-6100^(log100(2^32) + 10^-15) - 2^32 == 0.00001977log2(INT_MAX) > log10(INT_MAX)这意味着更大的对数基数,如果对数函数试图“搜索”一个正确的结果,它将很快达到由于四舍五入的错误而不再可能修改预测结果的情况。但是,这仍然是一个猜测。

还有其他计算对数的方法。例如,log10(x) == ln(x)/ln(10)如果对数函数以这种方式计算它,你会得到几乎相似的时间。

我的建议是(停止浪费时间),用时钟功能以外的方式描述你的程序(重新发明轮子是个坏主意,不使用现有的分析工具重新发明轮子,加上一个好的分析器将能够从日志函数中提供每行时序),读取日志函数的gcc源代码(毕竟它可用)和汇编输出。如果您不了解汇编输出,那么这将是学习如何阅读它的好机会。

如果拥有更快的对数函数真的很重要,并且算法优化真的不可能(如果对数确实是瓶颈,你可以缓存结果,例如)你可以尝试找到更快的算法实现,但如果我在这种情况下,你是否只是试图通过并行化任务来解决问题 - 例如。