需求:
当我们想要知道在复杂的项目结构中,是否存在频繁申请释放堆空间内存影响性能的操作?哪些代码哪个函数甚至哪一行进行了大量的堆区内存分配,程序内存分配的峰值落在哪个函数上?就可以使用valgrind中的massif工具来解析。
实战
首先说一下使用的操作
- 比如此时我的样例代码是test.cpp,在进行编译的时候记得加上-g:g++ test.cpp -g -o test
- 然后使用valgrind工具massif进行分析,valgrind –tool=massif ./test
- 在这里说一下两个重要的选项,一个是–time-unit=B,因为massif是定时获取快照的,如果获取的时间间隔比较大,就会记录信息不全,导致我们分析很困难,添加–time-unit=B来解决,valgrind –tool=massif –time-unit=B ./test。
- 另一个重要选项是–detailed-freq=1,如果我们发现自己的程序出现比较大幅度的堆空间变化,需要好好排查和思考是否可以优化,我们想要每次的快照都有详细的信息,可以增加参数–detailed-freq=1,valgrind –tool=massif –time-unit=B –detailed-freq=1 ./test。
- 此时生成massif.out.文件在当前目录下。堆massif.out文件的分析方式有两种:使用ms_print massif.out.<pid>会在控制台输出内容。使用massif-visualizer工具,massif-visualizer massif.out.<pid>,顺便提一嘴massif-visualizer 是需要单独安装的,Ubuntu上sudo apt install massif-visualizer,安装之后我们执行massif-visualizer可能会出现报错”核心转移“,此时输入export DISPLAY=:0即可,每次打开新的终端都需要输入export DISPLAY=:0,来告诉图形应用程序应该在什么地方寻找图形显示设备。
实例一:
#include <stdlib.h>
int main() {
const int array_size = 32;
void* p = malloc(array_size);
return 0;
}
使用massif-visualizer如果出现上述打印,说明启动成功,此时就可以打开虚拟机查看可视化分析结果。
可以看到随着时间的增加堆空间一直在增加,而且最终停在了32B处,这里就可以看到存在内存泄漏,我们修改下代码让程序没有内存泄漏再看一下会是什么结果。
#include <stdlib.h>
int main() {
const int array_size = 32;
void* p = malloc(array_size);
free(p);
return 0;
}
看图中呈现一个先上升后下降的趋势,并且最终快照3显示0B,说明此时内存全部释放掉了,通过上面两个小demo可以看出,massif虽然可以知道是否存在内存泄漏行为,但是并没有具体的内存泄漏的分析,这方面肯定是不如memcheck工具的,那massif的强大之处在哪呢?我们接着往下看。
实例2:
#include <stdlib.h>
void create_destory(unsigned int size) {
void *p = malloc(size);
free(p);
}
int main(void) {
const int loop = 4;
char* a[loop];
unsigned int kilo = 1024;
for (int i = 0; i < loop; i++) {
const unsigned int create_destory_size = 100 * kilo;
create_destory(create_destory_size);
}
return 0;
}
这里我多添加了一个编译选项,
查看结果:
这段代码频繁的申请和释放内存,肯定对程序的性能是有影响的,如果在繁杂的业务代码中,难以定位,但是我们使用massif-visualizer分析就很容易查看到,并且还可以在右侧快照处看到申请内存的代码在哪里。
实例三:
#include <stdlib.h>
void* create(unsigned int size) {
return malloc(size);
}
void create_destory(unsigned int size) {
void *p = create(size);
free(p);
}
int main(void) {
const int loop = 4;
char* a[loop];
unsigned int kilo = 1024;
for (int i = 0; i < loop; i++) {
const unsigned int create_size = 10 * kilo;
create(create_size);
const unsigned int malloc_size = 10 * kilo;
a[i] =(char*) malloc(malloc_size);
const unsigned int create_destory_size = 100 * kilo;
create_destory(create_destory_size);
}
for (int i = 0; i < loop; i++) {
free(a[i]);
}
return 0;
}
更贴近真实场景,融合了”堆分配”和”堆泄漏”的代码。
这里我标出了堆区内存图和右侧快照的对应关系,从快照3中可以看出此时堆区占有120KiB的内存,其中有两部分来源,一个是从test.cpp:4的create函数分配的110KiB,另一个是来自main函数中test.cpp:22行获取的堆区内存,快照5看到此时只有20KiB内存了,这是因为我们通过create_destory函数分配的堆区内存在分配之后就会释放,与我们的代码逻辑相符,B,C,D所对应的快照8,13,18中可以看到从test.cpp:22代码处分配的堆区内存是一直增加的,以及从test.cpp:19行申请的空间也是一直增加,这说明他们可能始终没有释放堆区内存有内存泄漏风险。
这是快照的后半部分,可以看到此时堆区内存一直在减少,来自test.cpp:22行所占有的堆区内存一直减少,这正好对应了我们代码中test.cpp:28的free释放的逻辑,而test.cpp:19行代码所占有的堆区内存始终没有释放,最终泄漏了40KiB的内存。
常见问题
fork
如果程序中使用了fork创建了多进程,我们在使用massif工具时,如果并没有自定义生成文件的名称,此时会产生多个massif文件分别对应每个进程的内存分析情况,但是如果我们使用–massif-out-file自定义文件名时,参数此时一定要加上%p,否则会出现不可预知的问题,对我们调试会造成困难。
#include <stdlib.h>
#include <unistd.h> // fork() 函数
#include <sys/wait.h>
void* create(unsigned int size) {
return malloc(size);
}
void create_destory(unsigned int size) {
void *p = create(size);
free(p);
}
void test1()
{
const int loop = 4;
char* a[loop];
unsigned int kilo = 1024;
for (int i = 0; i < loop; i++) {
const unsigned int create_size = 10 * kilo;
create(create_size);
const unsigned int malloc_size = 10 * kilo;
a[i] =(char*) malloc(malloc_size);
const unsigned int create_destory_size = 100 * kilo;
create_destory(create_destory_size);
}
for (int i = 0; i < loop; i++) {
free(a[i]);
}
}
void test2()
{
const int loop = 4;
char* a[loop];
unsigned int kilo = 1024;
for (int i = 0; i < loop; i++) {
const unsigned int create_size = 10 * kilo;
create(create_size);
const unsigned int malloc_size = 10 * kilo;
a[i] =(char*) malloc(malloc_size);
const unsigned int create_destory_size = 100 * kilo;
create_destory(create_destory_size);
}
for (int i = 0; i < loop; i++) {
free(a[i]);
}
exit(0);
}
int main(void) {
int pid = fork();
if(pid)
{
test1();
}
else
{
test2();
}
wait(NULL);
return 0;
}
测量进程中所有内存
在默认情况下massif仅测量堆区内存,使用malloc,calloc,realloc,new申请的内存,但一些比较低级的接口申请的堆区内存默认情况massif是不会测量的,比如mmap和brk,并且也不会测量代码、数据和BSS段的大小,所以massif报告的数据会比top查看到的程序内存小的多。如果想要测量程序使用的所有内存,可以使用-pages-as-heap=yes,此时会测量代码、数据和BSS段。并且较低级的系统调用mmap,brk的申请内存也会被计算。
查看栈区内存
如果想要查看栈区内存可以添加选项-stacks=yes
massif-visualizer
打开一个新终端,如果要使用massif-visualizer记得进行export DISPLAY=:0操作。
告诉图形应用程序去哪里寻找图形输出设备。