massif使用

  1. 背景
  2. massif
  3. 使用

背景

现在内存量大管饱, 优化几兆甚至几十兆, 大部分时候收益不高. 但同一个实例创造大量副本, 且这些副本共存的情况下(1000个), 优化单个实例内存的收益就会上升(1MBx1000=1GB).

进行内存优化, 首先要知晓一个实例都涉及了哪些内存分配, 由于涉及位置多和熟悉程度等原因, 导致人肉观察效率很低, 所以需要借助工具.

工具需要能够详细记录一个实例每次申请的内存大小和申请位置, 将申请大小降序排序之后, 能够方便的找到优化起来收益较大的点.

massif

massif就是这样的工具

样例代码

$ cat m1.cpp     
#include <map>
#include <thread>

int foo()
{
    std::map<int, int> m;
    for (int i = 0; i < 1000; ++i)
    {
        m[i] = i;
    }
    return 0;
}

int main()
{
    std::thread t([](){foo();});
    t.join();
    return 0;
}

如下是massif导出的结果, 可以看到有栈被打印出来, 后面会对这些内容做简要说明

$ cat 2.ms
  n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
  0      3,994,759           56,656           40,632        16,024            0
#### new分配了40,632B内存 ####
71.72% (40,632B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
#### 第一个组成部分 40,000B 用 -> 开头
->70.60% (40,000B) 0x4037D5: __gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<int const, int> > >::allocate(unsigned long, void const*) (new_allocator.h:104)
| ->70.60% (40,000B) 0x403401: std::_Rb_tree<int, std::pair<int const, int>, std::_Select1st<std::pair<int const, int> >, std::less<int>, std::allocator<std::pair<int const, int> > >::_M_get_node() (stl_tree.h:370)
|   ->70.60% (40,000B) 0x402B80: std::_Rb_tree_node<std::pair<int const, int> >* std::_Rb_tree<int, std::pair<int const, int>, std::_Select1st<std::pair<int const, int> >, std::less<int>, std::allocator<std::pair<int const, int> > >::_M_create_node<std::piecewise_construct_t const&, std::tuple<int const&>, std::tuple<> >(std::piecewise_construct_t const&, std::tuple<int const&>&&, std::tuple<>&&) (stl_tree.h:403)
|     ->70.60% (40,000B) 0x402913: std::_Rb_tree_iterator<std::pair<int const, int> > std::_Rb_tree<int, std::pair<int const, int>, std::_Select1st<std::pair<int const, int> >, std::less<int>, std::allocator<std::pair<int const, int> > >::_M_emplace_hint_unique<std::piecewise_construct_t const&, std::tuple<int const&>, std::tuple<> >(std::_Rb_tree_const_iterator<std::pair<int const, int> >, std::piecewise_construct_t const&, std::tuple<int const&>&&, std::tuple<>&&) (stl_tree.h:1669)
|       ->70.60% (40,000B) 0x40265D: std::map<int, int, std::less<int>, std::allocator<std::pair<int const, int> > >::operator[](int const&) (stl_map.h:465)
|         ->70.60% (40,000B) 0x40134E: foo() (m1.cpp:8)
|           ->70.60% (40,000B) 0x4013AE: main::{lambda()
|             ->70.60% (40,000B) 0x402243: void std::_Bind_simple<main::{lambda()
|               ->70.60% (40,000B) 0x40219A: std::_Bind_simple<main::{lambda()
|                 ->70.60% (40,000B) 0x402133: std::thread::_Impl<std::_Bind_simple<main::{lambda()
|                   ->70.60% (40,000B) 0x40ED4EF: execute_native_thread_routine (thread.cc:84)
|                     ->70.60% (40,000B) 0x5545EA4: start_thread (pthread_create.c:307)
|                       ->70.60% (40,000B) 0x5858B0C: clone (in /usr/lib64/libc-2.17.so)
|  #### 第二个组成部分 576B 用 -> 开头                       
->01.02% (576B) 0x4012784: allocate_dtv (dl-tls.c:317)
| ->01.02% (576B) 0x4012784: _dl_allocate_tls (dl-tls.c:533)
|   ->01.02% (576B) 0x554687B: allocate_stack (allocatestack.c:539)
|     ->01.02% (576B) 0x554687B: pthread_create@@GLIBC_2.2.5 (pthread_create.c:447)
|       ->01.02% (576B) 0x40ED73E: __gthread_create (gthr-default.h:662)
|         ->01.02% (576B) 0x40ED73E: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>) (thread.cc:142)
|           ->01.02% (576B) 0x4014CD: std::thread::thread<main::{lambda()
|             ->01.02% (576B) 0x4013CC: main (m1.cpp:15)
|               
->00.10% (56B) in 1+ places, all below ms_print's threshold (01.00%)

使用grep提取实际分配内存的大小和位置并进行排序, 可以看到0x4037D5分配了40000B的内存, 回上文中查找, 找到是0x40134E: foo() (m1.cpp:8)触发的分配.

$ grep "^->" 2.ms | grep -v "all below ms_print" | awk -F '[():]' '{gsub(",","");print $2,$3}' | sort -nr

40000B  0x4037D5
576B  0x4012784

假设上文是一个副本分配的内存, 再进行一次副本创建则会得到下面的结果.

$ grep "^->" 3.p | grep -v "all below ms_print" | awk -F '[():]' '{gsub(",","");print $2,$3}' | sort -nr

80000B  0x4037D5
576B  0x4012784

用两次0x4037D5位置的内存相减, 差值就是一个副本所需要的大小

将所有差值排序后 就是一个副本涉及的新增的内存, 可以根据地址从大到小逐一排查是否可以进行优化(如副本见共享同一份数据, 避免多次创建)

40000B 0x4037D5

使用

# threshold默认是1, 越小记录的精度越高, 如果过大会导致记录被合并无法展示细节, 这里指定的0,001
# m1是二进制名称 
valgrind --tool=massif --threshold=0.001 ./m1

问题: massif默认会记录程序从启动开始所有的内存, 且是基于无法控制且数量有限的快照机制

  1. 如果服务器需要较长时间初始化, 则会在初始化时消耗掉所有快照, 无法记录后续副本创建的分配.
  2. 快照是无法控制的, 无法方便的获得副本创建前后的内存消耗

所以需要手动进行快照

# A窗口
$ valgrind --tool=massif --threshold=0.001 --vgdb=yes --vgdb-error=0 ./m1
==28781== Massif, a heap profiler
==28781== Copyright (C) 2003-2017, and GNU GPL'd, by Nicholas Nethercote
==28781== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==28781== Command: ./m1
==28781== 
==28781== (action at startup) vgdb me ... 
==28781== 
==28781== TO DEBUG THIS PROCESS USING GDB: start GDB like this
==28781==   /path/to/gdb ./m1
==28781== and then give GDB the following command
==28781==   target remote | /usr/local/libexec/valgrind/../../bin/vgdb --pid=28781
==28781== --pid is optional if only one valgrind process is running
==28781== 
# B窗口
gdb ./m1
# 输入上文的提示
target remote | /usr/local/libexec/valgrind/../../bin/vgdb --pid=28781
# 副本创建之前下断点
b 6
# 运行
r
# 此时来到副本创建之前的位置
# 手动拍摄快照记录到1.m
monitor detailed_snapshot 1.m 

# 再次运行
# 此时将要创建第二个副本, 也就意味着第一个副本创建完毕
# 手动拍摄快照记录到2.m
monitor detailed_snapshot 2.m 

# 杀掉valgrind
monitor v.kill
# 退出gdb
quit

对快照的原始数据进行处理, 就得到了开头的2.ms文件和格式化后的文件, 使用Excel对1.mp和2.mp相同地址的部分计算差值即可.

ms_print 1.m > 1.ms
ms_print 2.m > 2.ms

grep "^->" 1.ms | grep -v "all below ms_print" | awk -F '[():]' '{gsub(",","");print $2,$3}' | sort -nr > 1.mp
grep "^->" 2.ms | grep -v "all below ms_print" | awk -F '[():]' '{gsub(",","");print $2,$3}' | sort -nr > 2.mp