堆问题分析利器——valgrind的massif

需求:

当我们想要知道在复杂的项目结构中,是否存在频繁申请释放堆空间内存影响性能的操作?哪些代码哪个函数甚至哪一行进行了大量的堆区内存分配,程序内存分配的峰值落在哪个函数上?就可以使用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操作。
告诉图形应用程序去哪里寻找图形输出设备。

如果觉得本文对您有所帮助,可以支持下博主,一分也是缘分😊
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇