【问题标题】:KCachegrind output for optimized vs unoptimized builds优化与未优化构建的 KCachegrind 输出
【发布时间】:2018-07-24 13:08:41
【问题描述】:

我在下面代码生成的可执行文件上运行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 中查看这些文件(一个用于调试可执行文件,另一个用于发布可执行文件)时,调试构建是可以理解的,如下所示:

正如所料,函数XYZ::Count() const被调用了10001次。然而,优化的发布版本更难破译,而且根本不清楚该函数被调用了多少次。我知道函数调用可能是inlined。但是如何确定它实际上已经被内联了呢?发布版本的调用图如下所示:

main() 似乎根本没有函数 XYZ::Count() const 的迹象。

我的问题是:

(1)如果不查看调试/发布版本生成的汇编语言代码,以及使用 KCachegrind,如何计算一个特定函数(在本例中为 XYZ::Count() const)被调用了多少次?在上面的发布构建调用图中,该函数甚至没有被调用一次。

(2)有没有办法了解 KCachegrind 为发布/优化构建提供的调用图和其他详细信息?我已经查看了https://docs.kde.org/trunk5/en/kdesdk/kcachegrind/kcachegrind.pdf 上提供的 KCachegrind 手册,但我想知道是否有一些有用的技巧/经验法则可以在发布版本中寻找。

【问题讨论】:

    标签: c++ optimization valgrind kcachegrind


    【解决方案1】:

    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 与 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>
    

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

    【讨论】:

    • 感谢您的详细回复。看起来我应该只做普通的 C++ 编码,而不用太担心编译器是否真的做了它在发布版本中应该做的事情。我将参考这个并查看发布版本的 asm,以满足我未来的好奇心。
    【解决方案2】:

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

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

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

    【讨论】:

    • release .out 文件不包含 Count() 的实例,但调试 .out 确实包含 Count()
    • 这不是不看asm,而是在valgrind手册中:“如果您希望能够看到汇编代码级别的注释,请指定--dump-instr=yes。这将在指令中生成配置文件数据粒度。请注意,生成的配置文件数据只能使用 KCachegrind 查看。对于程序集注释,查看函数内部控制流的更多细节也很有趣,即(条件)跳转。这将通过进一步指定来收集—— collect-jumps=yes。”
    猜你喜欢
    • 1970-01-01
    • 2017-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-20
    • 1970-01-01
    • 2020-11-15
    相关资源
    最近更新 更多