在上篇文章中我介绍了callgrind
的大致用法,可以看出来,callgrind
是一个非侵入式的,使用起来也很傻瓜的调优工具。初用时感觉这个工具非常趁手,是个程序都想用callgrind
去分析一下。但深入使用后发现,callgrind
不是银弹,它还是有一些缺陷的。这些缺陷的根源在于:callgrind
使用指令数来衡量性能,而程序员用耗时来衡量性能,指令数与耗时仅仅是一个正相关的关系,而非成比例的关系,这就导致了用插时间戳统计出来的性能数据,和用callgrind
统计出来的性能数据有出入。为了说明这些出入,我举3个实例。
软硬件环境:
OS: Ubuntu 18.04.3(Linux 4.15.0-58-generic)
CPU: Intel® Core™ i5-8250U @1.60GHz
Compiler: gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)
cache的影响
对于二维数组,使用行优先访问要比使用列优先要快,这是CPU cache的功劳。看下面的例子:
#include "tool.h"
#include <vector>
const int SIZE = 2000;
std::vector<double> rowFirstVector;
std::vector<double> columnFirstVector;
double rowFirst()
{
Logger l("row first access");
double total;
for (int i = 0; i < SIZE; i++)
{
for (int j = 0; j < SIZE; j++)
{
total += rowFirstVector[i * SIZE + j];
}
}
return total;
}
double columnFirst()
{
Logger l("column first access");
double total;
for (int i = 0; i < SIZE; i++)
{
for (int j = 0; j < SIZE; j++)
{
total += columnFirstVector[j * SIZE + i];
}
}
return total;
}
int main()
{
rowFirstVector = std::vector<double>(SIZE * SIZE);
rowFirst();
columnFirstVector = std::vector<double>(SIZE * SIZE);
columnFirst();
return 0;
}
代码中的Logger
类用来记录时间戳打印耗时数据,使用了RAII
的手法来实现。由于二维数组在内存中也是线性的,所以就用vector
来代替了。rowFirst
使用行优先访问内存,columnFirst
使用列优先访问内存,开启O2优化编译源码,运行结果如下:
row first access cost 15.018 ms
column first access cost 22.841 ms
行优先比列优先快,符合我们的预期。而callgrind
却得到了这样的结果:
在callgrind
看来,rowFirst
和columnFirst
的性能平分秋色,因为他们执行了相同多的指令。
当然,在使用callgrind
的时候可以打开cache仿真来统计cache miss的数据,但是这个数据无法与指令计数数据综合在一起,只能各自比较,并不直观。
单指令耗时的影响
浮点指令比整数指令要慢(至少在我的机器上是),所以有了这个例子:
#include "tool.h"
#include <vector>
const int SIZE = 10000000;
std::vector<int> intVector;
std::vector<double> doubleVector;
int intMultiply()
{
Logger l("intMultiply");
int result;
for (int i = 0; i < SIZE; i++)
{
result += intVector[i] * 1;
}
return result;
}
double doubleMultiply()
{
Logger l("doubleMultiply");
double result;
for (int i = 0; i < SIZE; i++)
{
result += doubleVector[i] * 1.0;
}
return result;
}
int main()
{
intVector = std::vector<int>(SIZE);
intMultiply();
doubleVector = std::vector<double>(SIZE);
doubleMultiply();
return 0;
}
执行结果为如下,符合我们的预期:
intMultiply cost 7.388 ms
doubleMultiply cost 29.994 ms
而callgrind的结果是:
没错,仍然分不出来哪个快哪个慢,还是因为它们执行了相同多的指令。
系统调用性能无法统计
关于这一条,主要是受callgrind
实现机制所限,callgrind
实际上是实现了一个虚拟机,对程序每条汇编指令解释执行同时计数。但是对于系统调用没办法解释执行啊,只能交给真实的CPU去执行,在内核空间的系统调用实现也都会在真实CPU上执行,callgrind
无法统计它们的指令数量。因此我们有了下面的例子:
#include "tool.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
const int MB_BYTES = 1024 * 1024;
void read1M()
{
Logger l("read1M");
int fd = open("./1M.txt", O_RDONLY);
assert(fd >= 0);
const int SIZE = 1 * MB_BYTES;
char *buf = static_cast<char *>(malloc(SIZE));
int result = read(fd, buf, SIZE);
assert(result = SIZE);
close(fd);
}
void read10M()
{
Logger l("read10M");
int fd = open("./10M.txt", O_RDONLY);
assert(fd >= 0);
const int SIZE = 10 * MB_BYTES;
char *buf = static_cast<char *>(malloc(SIZE));
int result = read(fd, buf, SIZE);
assert(result = SIZE);
close(fd);
}
int main()
{
read1M();
read10M();
return 0;
}
1M.txt是一个大小为1MB的文件,10M.txt是一个大小为10MB的文件,我们会非常自然地期望读取前者比读取后者要快,即使速度与大小不见得成比例。执行结果如下,符合我们的预期:
read1M cost 1.801 ms
read10M cost 8.935 ms
callgrind的统计结果却是这样的:
总结
这里我列举出来3个因素来说明问题,实际可能有更多的因素。可以想想,程序规模大了以后,这些因素会一起叠加起来影响callgrind
的统计结果,导致数据参考价值降低。
我当然不是说callgrind
一无是处,只是当老板问你哪个模块是性能瓶颈的时候,你可千万别直接丢一份callgrind
统计的数据过去,这个数据我们自己参考就行。