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

C++ KCachegrind输出用于优化与未优化的构建,c++,optimization,valgrind,kcachegrind,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; };

我在由以下代码生成的可执行文件上运行
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;
}
#包括
#包括
使用名称空间std;
XYZ类{
公众:
int Count()常量{return Count;}
无效计数(int val){Count=val;}
私人:
整数计数;
};
int main(){
XYZ-XYZ;
xyz.计数(10000);
整数和=0;
对于(int i=0;i
我使用以下选项进行
debug
构建:
-fPIC-fno strict aliasing-feexceptions-g-std=c++14
发行版
版本具有以下选项:
-fPIC-fno严格别名-feexceptions-g-O2-std=c++14

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

正如预期的那样,函数
XYZ::Count()const
被调用了10001次。然而,优化后的发布版本更难解读,而且不清楚函数被调用了多少次。我知道函数调用可能是内联的。但是,人们如何判断它实际上是内联的呢?发布版本的调用图如下所示:

main()
中似乎根本没有函数
XYZ::Count()const
的指示

我的问题是:

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


(2) 有没有办法了解KCachegrind为发布/优化版本提供的调用图和其他详细信息?我已经看过了上提供的KCachegrind手册,但我想知道是否有一些有用的技巧/经验法则可以在发布版本中查找。

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

grep "XYZ::Count()" callgrind.out | more
如果您在callgrind文件中找到函数名,那么重要的是要知道kcachegrind隐藏了权重较小的函数。
请参见:

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

问题是,你所说的打电话是什么意思?如果一个函数是内联的,它仍然被“调用”吗?事实上,情况更复杂,乍一看,你的例子并不是那么简单

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

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

那么,
Count()
是内联的吗?是的,在优化的最初步骤中的某个地方,但随后代码变得完全不同-在最终的汇编程序中没有内联
Count()
的地方

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

...
int main(int argc,  char* argv[]) {
   XYZ xyz;
   xyz.Count(atoi(argv[1]));
...
在结果中,我们仍然没有遇到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"
优化器为求和i=0..n-1
得出了公式
(n-1)*(n-2)/2

现在,让我们将
Count()
的定义隐藏在单独的翻译单元
class.cpp
中,这样优化器就看不到它的定义:

class XYZ{
public:
    int Count() const;//definition in separate translation unit
...
现在我们得到for循环,并在每次迭代中调用
Count()
,最重要的部分是:

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

然而,对于现代的工具链来说,仅仅看到单个翻译单元的汇编程序是不够的——有一种叫做
链接时间优化
。我们可以通过沿着以下路线在某处建造来使用它:

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安装似乎与lto有问题):


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

release.out文件不包含Count()的实例,但debug.out包含Count()。这不是没有查看asm,而是在valgrind手册中:如果希望能够看到程序集代码级注释,请指定--dump instr=yes。这将以指令粒度生成配置文件数据。请注意,生成的配置文件数据只能使用KCachegrind查看。对于程序集注释,还可以看到函数内部控制流的更多细节,即(条件)跳转。这将通过进一步指定收集跳跃=是。谢谢您的详细响应。看起来我应该只做普通的C++编码,而不用担心编译器实际上是不是在发布版本中应该做什么。我将参考这个并查看ASM的RE。
.L6:
        addl    %ebx, %ebp
        addl    $1, %ebx
.L3:
        movq    %r12, %rdi
        call    XYZ::Count() const@PLT
        cmpl    %eax, %ebx
        jl      .L6
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
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)
  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>