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

Isk*_*rak 29 c++ math performance gcc

我需要在我的一些代码中使用对数函数,但基数并不重要.于是我开始挑之间log(),log2()log10()通过性能,提供我发现任何显著差异.(我将参照所述功能ln,lblg分别地).

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

jan*_*neb 6

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

更改示例以便我添加一行

cout << "sum=" << sum << endl;
Run Code Online (Sandbox Code Playgroud)

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

  • 日志: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
Run Code Online (Sandbox Code Playgroud)

除main之外,所有其他符号都与log()调用相关.总结这些,我们可以得出结论,此示例的总运行时间的97%用于log().

可以在glibc git repo中找到__ieee754_log的实现.相应地,其他实现是:log2,log10.请注意,之前的链接是针对HEAD版本的,对于已发布的版本,请参阅其相应的分支


csc*_*wan 5

我注意到一些事情。如果我编译(GCC 4.5.3)你的汇编器列表-O3 -Sg++ logflt.S -lrt我可以重现该行为。我的时间安排是:

ln=6:984160044
lb=6:950842852
lg=3:64288522
Run Code Online (Sandbox Code Playgroud)

然后我检查了输出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
Run Code Online (Sandbox Code Playgroud)

0x460(%rip)是指向十六进制值的相对地址0000 00000000 33333333 33332440。这是一个 16 字节的 SSEdouble对,其中只有一个 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
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但不会为loglog2


Gun*_*iez 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
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)