KCachegrind输出用于优化与未优化的构建

时间:2018-02-14 03:17:25

标签: c++ optimization valgrind kcachegrind

我在由以下代码生成的可执行文件上运行valgrind --tool=callgrind ./executable

#include <cstdlib>
#include <stdio.h>
using namespace std;

class XYZ{
public:
    int Count() const {return count;}
    void Count(int val){count = val;}
private:
    int count;
};

int main() {
    XYZ xyz;
    xyz.Count(10000);
    int sum = 0;
    for(int i = 0; i < xyz.Count(); i++){
//My interest is to see how the compiler optimizes the xyz.Count() call
        sum += i;
    }
    printf("Sum is %d\n", sum);
    return 0;
}

我使用以下选项制作debug版本:-fPIC -fno-strict-aliasing -fexceptions -g -std=c++14release版本具有以下选项:-fPIC -fno-strict-aliasing -fexceptions -g -O2 -std=c++14

运行valgrind会生成两个转储文件。当在KCachegrind中查看这些文件(一个用于调试可执行文件,另一个用于发行版可执行文件)时,调试版本是可以理解的,如下所示:

Debug Build

正如预期的那样,函数XYZ::Count() const被称为10001次。但是,优化的版本构建更难以破译,并且不清楚函数被调用多少次。我知道函数调用可能是inlined。但是,如何弄清楚它是否已被内联?发布版本的调用图如下所示:

Release Build

XYZ::Count() const似乎没有任何功能main()的迹象。

我的问题是:

(1)在不查看调试/发布版本生成的汇编语言代码的情况下,通过使用KCachegrind,如何确定特定函数(在本例中为XYZ::Count() const)的次数是多少?叫什么名字?在上面的发布构建调用图中,该函数甚至不会被调用一次。

(2)有没有办法理解KCachegrind为发布/优化版本提供的调用图和其他细节?我已经查看了https://docs.kde.org/trunk5/en/kdesdk/kcachegrind/kcachegrind.pdf提供的KCachegrind手册,但我想知道是否有一些有用的黑客/经验法则应该在发布版本中寻找。

2 个答案:

答案 0 :(得分:2)

valgrind的输出很容易理解:正如valgrind + kcachegrind告诉你的那样,在发布版本中根本没有调用这个函数。

问题是,你叫什么意思?如果内联函数,它是否仍然“被调用”?实际上,情况更复杂,因为它看起来乍一看,你的榜样并非那么微不足道。

发布版本中是否Count()内联了?当然,有点。优化过程中的代码转换通常非常显着,就像你的情况一样 - 最好的判断方法是查看结果assembler(这里是clang):

main:                                   # @main
        pushq   %rax
        leaq    .L.str(%rip), %rdi
        movl    $49995000, %esi         # imm = 0x2FADCF8
        xorl    %eax, %eax
        callq   printf@PLT
        xorl    %eax, %eax
        popq    %rcx
        retq
.L.str:
        .asciz  "Sum is %d\n"

你可以看到,main根本没有执行for循环,只是打印结果(49995000),这是在优化过程中计算的,因为迭代次数是在编译期间已知。

Count()内联了吗?是的,在优化的第一步中的某个地方,但随后代码变得完全不同 - 在最终汇编程序中没有Count()内联的位置。

那么当我们“隐藏”编译器的迭代次数时会发生什么?例如。通过命令行传递它:

...
int main(int argc,  char* argv[]) {
   XYZ xyz;
   xyz.Count(atoi(argv[1]));
...

在生成的assembler中,我们仍然没有遇到for循环,因为优化器可以判断出Count()的调用没有副作用并优化了整个事情:

main:                                   # @main
        pushq   %rbx
        movq    8(%rsi), %rdi
        xorl    %ebx, %ebx
        xorl    %esi, %esi
        movl    $10, %edx
        callq   strtol@PLT
        testl   %eax, %eax
        jle     .LBB0_2
        leal    -1(%rax), %ecx
        leal    -2(%rax), %edx
        imulq   %rcx, %rdx
        shrq    %rdx
        leal    -1(%rax,%rdx), %ebx
.LBB0_2:
        leaq    .L.str(%rip), %rdi
        xorl    %eax, %eax
        movl    %ebx, %esi
        callq   printf@PLT
        xorl    %eax, %eax
        popq    %rbx
        retq
.L.str:
        .asciz  "Sum is %d\n"

优化程序为总和(n-1)*(n-2)/2提供了公式i=0..n-1

现在让我们在单独的翻译单元Count()中隐藏class.cpp的定义,因此优化程序无法看到它的定义:

class XYZ{
public:
    int Count() const;//definition in separate translation unit
...

现在我们在每次迭代中获得for循环并调用Count()the assembler最重要的部分是:

.L6:
        addl    %ebx, %ebp
        addl    $1, %ebx
.L3:
        movq    %r12, %rdi
        call    XYZ::Count() const@PLT
        cmpl    %eax, %ebx
        jl      .L6

Count()(在%rax中)的结果与每个迭代步骤中的当前计数器(在%ebx中)进行比较。现在,如果我们使用valgrind运行它,我们可以在被调用者列表中看到,XYZ::Count()被称为10001次。

然而,对于现代工具链来说,仅仅看到单个翻译单元的汇编程序是不够的 - 有一个名为link-time-optimization的东西。我们可以通过沿着这些方向构建某个地方来使用它:

gcc -fPIC -g -O2 -flto -o class.o -c class.cpp
gcc -fPIC -g -O2 -flto -o test.o  -c test.cpp
gcc -g -O2 -flto -o test_r class.o test.o

使用valgrind运行生成的可执行文件,我们再次看到,Count()未被调用!

然而查看机器代码(这里我使用gcc,我的clang-installation似乎与lto有问题):

00000000004004a0 <main>:
  4004a0:   48 83 ec 08             sub    $0x8,%rsp
  4004a4:   48 8b 7e 08             mov    0x8(%rsi),%rdi
  4004a8:   ba 0a 00 00 00          mov    $0xa,%edx
  4004ad:   31 f6                   xor    %esi,%esi
  4004af:   e8 bc ff ff ff          callq  400470 <strtol@plt>
  4004b4:   85 c0                   test   %eax,%eax
  4004b6:   7e 2b                   jle    4004e3 <main+0x43>
  4004b8:   89 c1                   mov    %eax,%ecx
  4004ba:   31 d2                   xor    %edx,%edx
  4004bc:   31 c0                   xor    %eax,%eax
  4004be:   66 90                   xchg   %ax,%ax
  4004c0:   01 c2                   add    %eax,%edx
  4004c2:   83 c0 01                add    $0x1,%eax
  4004c5:   39 c8                   cmp    %ecx,%eax
  4004c7:   75 f7                   jne    4004c0 <main+0x20>
  4004c9:   48 8d 35 a4 01 00 00    lea    0x1a4(%rip),%rsi        # 400674 <_IO_stdin_used+0x4>
  4004d0:   bf 01 00 00 00          mov    $0x1,%edi
  4004d5:   31 c0                   xor    %eax,%eax
  4004d7:   e8 a4 ff ff ff          callq  400480 <__printf_chk@plt>
  4004dc:   31 c0                   xor    %eax,%eax
  4004de:   48 83 c4 08             add    $0x8,%rsp
  4004e2:   c3                      retq   
  4004e3:   31 d2                   xor    %edx,%edx
  4004e5:   eb e2                   jmp    4004c9 <main+0x29>
  4004e7:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)

我们可以看到,对函数Count()的调用是内联的,但是仍然有一个for循环(我猜这是一个gcc vs clang的事情)。

但是你最感兴趣的是:函数Count()只被“调用”一次 - 它的值被保存到寄存器%ecx,而循环实际上只是:

  4004c0:   01 c2                   add    %eax,%edx
  4004c2:   83 c0 01                add    $0x1,%eax
  4004c5:   39 c8                   cmp    %ecx,%eax
  4004c7:   75 f7                   jne    4004c0 <main+0x20>

如果使用选项`--dump-instr = yes运行valgrind,你可以在Kcachegrid的帮助下看到这一切。

答案 1 :(得分:0)

在callgrind.out文件中搜索XYZ :: Count()以查看valgrind是否记录了此函数的任何事件。

grep "XYZ::Count()" callgrind.out | more

如果在callgrind文件中找到函数名,那么重要的是要知道kcachegrind隐藏了权重较小的函数。 请参阅:Make callgrind show all function calls in the kcachegrind callgraph

上的答案