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>