简介
Massif是一个堆分析器。它度量程序使用了多少堆内存。这包括有用的空间,以及分配给簿记和对齐目的的额外字节。它还可以测量程序堆栈的大小,尽管默认情况下它不这样做。
堆分析可以帮助您减少程序使用的内存量。在具有虚拟内存的现代机器上,这提供了以下好处:
它可以加快程序的速度——较小的程序将更好地与机器的缓存进行交互,并避免分页。
如果您的程序使用大量内存,它将减少耗尽计算机交换空间的机会。
此外,还有一些空间泄漏是传统的泄漏检查器无法检测到的,比如Memcheck。这是因为内存实际上并没有丢失——指针仍然指向它——但它没有被使用。具有此类泄漏的程序会随着时间的推移不必要地增加它们所使用的内存量。Massif可以帮助识别这些泄漏。
重要的是,Massif不仅告诉您程序使用了多少堆内存,还提供了非常详细的信息,指出程序的哪些部分负责分配堆内存。
Massif还使用命令行选项–xtree-memory和监视器命令xtmemory提供执行树内存分析。
使用Massif和ms_print
首先,对于其他Valgrind工具,应该使用调试信息(-g选项)进行编译。用什么优化级别编译程序并不重要,因为这不太可能影响堆内存的使用。
然后,您需要运行Massif本身来收集分析信息,然后运行ms_print以可读的方式显示它。
An Example Program
举个例子就能说明问题。考虑下面的C程序(带有行号注释),它在堆上分配许多不同的块。
1 #include <stdlib.h>
2
3 void g(void)
4 {
5 malloc(4000);
6 }
7
8 void f(void)
9 {
10 malloc(2000);
11 g();
12 }
13
14 int main(void)
15 {
16 int i;
17 int* a[10];
18
19 for (i = 0; i < 10; i++) {
20 a[i] = malloc(1000);
21 }
22
23 f();
24
25 g();
26
27 for (i = 0; i < 10; i++) {
28 free(a[i]);
29 }
30
31 return 0;
32 }
Running Massif
valgrind --tool=massif prog
程序将(缓慢地)执行。完成后,Valgrind的评论没有汇总统计;所有Massif的分析数据都被写入一个文件。默认情况下,这个文件名为mass .out.<pid>是进程ID,尽管这个文件名可以通过–mass -out-file选项更改。
Running ms_print
ms_print massif.out.12345
Ms_print将生成(a)一个显示程序执行过程中内存消耗的图表,以及(b)关于程序中各个点上负责分配的站点的详细信息,包括内存分配峰值点。使用单独的脚本来表示结果是有意为之的:它将数据收集与表示分开,这意味着将来可以添加表示数据的新方法。
The Output Preamble
在Massif下运行这个程序后,ms_print输出的第一部分包含一个序言,它只是说明了程序、Massif和ms_print是如何分别被调用的:
--------------------------------------------------------------------------------
Command: example
Massif arguments: (none)
ms_print arguments: massif.out.12797
--------------------------------------------------------------------------------
The Output Graph
下一部分是显示程序执行时内存消耗情况的图表:
KB
19.63^ #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| #
| :#
| :#
| :#
0 +----------------------------------------------------------------------->ki 0 113.4
Number of snapshots: 25
Detailed snapshots: [9, 14 (peak), 24]
为什么图的大部分是空的,只有最后的几条呢?默认情况下,Massif使用“执行的指令”作为时间单位。对于像示例这样运行时间非常短的程序,大多数执行的指令都涉及到程序的加载和动态链接。main的执行(以及堆分配)只发生在最后。对于这样一个短时间运行的程序,我们可以使用–time-unit=B选项来指定时间单位为在堆和堆栈上分配/释放的字节数。
如果我们用这个选项在Massif下重新运行程序,然后重新运行ms_print,我们会得到这个更有用的图:
19.63^ ###
| #
| # ::
| # : :::
| :::::::::# : : ::
| : # : : : ::
| : # : : : : :::
| : # : : : : : ::
| ::::::::::: # : : : : : : :::
| : : # : : : : : : : ::
| ::::: : # : : : : : : : : ::
| @@@: : : # : : : : : : : : : @
| ::@ : : : # : : : : : : : : : @
| :::: @ : : : # : : : : : : : : : @
| ::: : @ : : : # : : : : : : : : : @
| ::: : : @ : : : # : : : : : : : : : @
| :::: : : : @ : : : # : : : : : : : : : @
| ::: : : : : @ : : : # : : : : : : : : : @
| :::: : : : : : @ : : : # : : : : : : : : : @
| ::: : : : : : : @ : : : # : : : : : : : : : @
0 +----------------------------------------------------------------------->KB 0 29.48
Number of snapshots: 25
Detailed snapshots: [9, 14 (peak), 24]
图的大小可以通过ms_print的–x和–y选项来改变。每个竖条表示一个快照,即在某个时间点对内存使用情况的测量。如果下一个快照与下一个快照列的距离超过一列,则从快照的顶部到下一个快照列的前面绘制一条水平的字符行。底部的文本显示了该程序的25个快照,即每个堆分配/回收一个快照,外加几个额外的快照。Massif开始时为每一个堆分配/回收执行快照,但是随着程序运行时间的延长,它执行快照的频率会降低。随着程序的进行,它也会丢弃旧的快照;当它达到快照的最大数量(默认为100个,但可以通过–max-snapshots选项进行更改)时,将删除一半快照。这意味着总是维护合理数量的快照。
大多数快照都是正常的,只记录基本信息。普通快照在图中由“:”字符组成的条表示。
有些快照是详细的。关于分配发生的位置的信息将为这些快照记录下来,稍后我们将看到这一点。详细的快照在图中由“@”字符组成的柱状图表示。底部的文本显示了该程序的3个详细快照(快照9、14和24)。默认情况下,每10个快照都是详细的,尽管这可以通过–details -freq选项来更改。
最后,最多有一个峰值快照。峰值快照是一个详细的快照,记录内存消耗最大的点。峰值快照在图中由“#”字符组成的条表示。底部的文本显示快照14是峰值。
Massif对峰值发生时间的判断可能是错误的,原因有二。
-
峰值快照只在发生释放之后才会被捕获。这避免了大量不必要的峰值快照记录(想象一下,如果您的程序连续分配大量堆块,每次都达到一个新的峰值会发生什么)。但这意味着,如果你的程序从不释放任何块,就不会记录峰值。这也意味着,如果你的程序释放块,但后来分配到一个更高的峰值没有随后释放,报告的峰值将太低。
-
即使有这种行为,准确记录峰值也是缓慢的。所以在默认情况下,Massif记录了一个峰值,其大小在真实峰值大小的1%以内。峰值测量中的这种不准确性可以通过–peak-inaccuracy选项来改变。
下图来自KDE web浏览器Konqueror的一次执行。它展示了大型程序的图形是什么样子的。
MB
3.952^ #
| @#:
| :@@#:
| @@::::@@#:
| @ :: :@@#::
| @@@ :: :@@#::
| @@:@@@ :: :@@#::
| :::@ :@@@ :: :@@#::
| : :@ :@@@ :: :@@#::
| :@: :@ :@@@ :: :@@#::
| @@:@: :@ :@@@ :: :@@#:::
| : :: ::@@:@: :@ :@@@ :: :@@#:::
| :@@: ::::: ::::@@@:::@@:@: :@ :@@@ :: :@@#:::
| ::::@@: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| @: ::@@: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| @: ::@@: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| @: ::@@:::::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| ::@@@: ::@@:: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| :::::@ @: ::@@:: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
| @@:::::@ @: ::@@:: ::: ::::::: @ :::@@:@: :@ :@@@ :: :@@#:::
0 +----------------------------------------------------------------------->Mi
0 626.4
Number of snapshots: 63
Detailed snapshots: [3, 4, 10, 11, 15, 16, 29, 33, 34, 36, 39, 41,
42, 43, 44, 49, 50, 51, 53, 55, 56, 57 (peak)]
注意,较大的大小单位是KB、MB、GB等。与典型的内存测量一样,这些测试基于1024的乘数,而不是标准的SI乘数1000。严格地说,它们应该被写成KiB、MiB、GiB等。
快照详细信息
回到我们的示例,图后面是每个快照的详细信息。前9个快照是正常的,所以每个快照只记录少量的信息:
--------------------------------------------------------------------------------
n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B)
--------------------------------------------------------------------------------
0 0 0 0 0 0
1 1,008 1,008 1,000 8 0
2 2,016 2,016 2,000 16 0
3 3,024 3,024 3,000 24 0
4 4,032 4,032 4,000 32 0
5 5,040 5,040 5,000 40 0
6 6,048 6,048 6,000 48 0
7 7,056 7,056 7,000 56 0
8 8,064 8,064 8,000 64 0
每个普通快照都记录了一些东西。
-
它的数量。
-
拍摄的时间。在本例中,时间单位是字节,因为使用了–time-unit=B。
-
该点的总内存消耗。
-
在该点分配的有用堆字节数。这反映了程序所要求的字节数。
-
在该点分配的额外堆字节数。这反映分配的字节数超过程序要求的。有两个额外堆字节的来源。
-
首先,每个堆块都有与之关联的管理字节。管理字节的确切数量取决于分配器的详细信息。在默认情况下,Massif假设每个块有8个字节,从示例中可以看到,但这个数字可以通过–heap-admin选项更改。
-
其次,分配器经常将请求的字节数四舍五入到更大的数字,通常是8或16。这是确保块中的元素适当对齐所必需的。如果需要N个字节,则Massif将N四舍五入到由–alignment选项指定的值的最接近倍数。
-
堆栈的大小。默认情况下,堆栈分析是关闭的,因为它会大大降低Massif的速度。因此,本例中的堆栈列为零。可以通过–stacks=yes选项开启堆栈分析。
下一个快照是详细的。除了基本的计数,它还给出了一个分配树,精确地指出了哪些代码段负责分配堆内存:
9 9,072 9,072 9,000 72 0
99.21% (9,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->99.21% (9,000B) 0x804841A: main (example.c:20)
分配树可以从上到下读取。第一行表示所有堆分配函数,如malloc和c++ new。所有堆分配都要经过这些函数,因此所有9000个有用字节(占所有已分配字节的99.21%)都要经过它们。但是malloc和new是怎么称呼的呢?在这一点上,到目前为止的每个分配都是由于main中的第20行,因此是树中的第二行。比;指示main(第20行)调用malloc。
让我们看看接下来发生了什么:
--------------------------------------------------------------------------------
n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B)
--------------------------------------------------------------------------------
10 10,080 10,080 10,000 80 0
11 12,088 12,088 12,000 88 0
12 16,096 16,096 16,000 96 0
13 20,104 20,104 20,000 104 0
14 20,104 20,104 20,000 104 0
99.48% (20,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->49.74% (10,000B) 0x804841A: main (example.c:20)
|
->39.79% (8,000B) 0x80483C2: g (example.c:5)
| ->19.90% (4,000B) 0x80483E2: f (example.c:11)
| | ->19.90% (4,000B) 0x8048431: main (example.c:23)
| |
| ->19.90% (4,000B) 0x8048436: main (example.c:25)
|
->09.95% (2,000B) 0x80483DA: f (example.c:10)
->09.95% (2,000B) 0x8048431: main (example.c:23)
前四个快照与前面的快照相似。但随后到达全局分配峰值,并获取详细快照(第14个快照)。它的分配树显示已经分配了20,000B有用的堆内存,并且这些行和箭头表明这来自三个不同的代码位置:第20行,负责10,000B (49.74%);第五行,负责8000个b (39.79%);第十行,负责2000 b(9.95%)。
然后我们可以在分配树中进一步深入。例如,在第5行请求的8000个b中,有一半来自第11行,一半来自第25行。
简而言之,Massif将程序中每一个分配点的堆栈跟踪整理成一个树,它给出了在特定时间点如何以及为什么分配所有堆内存的完整图像。
注意,树条目不是对应于函数,而是对应于单个代码位置。例如,如果函数A调用malloc,函数B调用A两次,一次在第10行,一次在第11行,那么这两次调用将在树中产生两个不同的堆栈跟踪。相反,如果B从第15行重复调用A(例如,由于循环),那么每个调用都将由树中的相同堆栈跟踪表示。
还请注意,示例中每个带有子条目的树条目都满足一个不变量:条目的大小等于其子条目的大小之和。例如,第一个条目的大小为20,000B,其子条目的大小为10,000B、8,000B和2,000B。一般来说,这个不变量几乎总是成立的。但是,在极少数情况下,堆栈跟踪可能是畸形的,在这种情况下,一个堆栈跟踪可能是另一个堆栈跟踪的子跟踪。这意味着树中的一些条目可能不满足不变量——条目的大小将大于其子条目的大小之和。这不是一个大问题,但可能会导致结果混乱。当这种情况发生时,Massif有时可以检测到;如果是,则发出警告:
Warning: Malformed stack trace detected. In Massif's output,
the size of an entry's child entries may not sum up
to the entry's size as they normally do.
然而,Massif并不会发现和警告每一个这样的事件。幸运的是,在实践中,畸形堆栈痕迹很少。
现在返回到ms_print的输出,最后的部分类似:
--------------------------------------------------------------------------------
n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B)
--------------------------------------------------------------------------------
15 21,112 19,096 19,000 96 0
16 22,120 18,088 18,000 88 0
17 23,128 17,080 17,000 80 0
18 24,136 16,072 16,000 72 0
19 25,144 15,064 15,000 64 0
20 26,152 14,056 14,000 56 0
21 27,160 13,048 13,000 48 0
22 28,168 12,040 12,000 40 0
23 29,176 11,032 11,000 32 0
24 30,184 10,024 10,000 24 0
99.76% (10,000B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->79.81% (8,000B) 0x80483C2: g (example.c:5)
| ->39.90% (4,000B) 0x80483E2: f (example.c:11)
| | ->39.90% (4,000B) 0x8048431: main (example.c:23)
| |
| ->39.90% (4,000B) 0x8048436: main (example.c:25)
|
->19.95% (2,000B) 0x80483DA: f (example.c:10)
| ->19.95% (2,000B) 0x8048431: main (example.c:23)
|
->00.00% (0B) in 1+ places, all below ms_print's threshold (01.00%)
最后的详细快照显示了堆在终止时的情况。00.00%条目表示分配和释放内存的代码位置(在本例中,第20行是在第28行释放的内存)。但是,没有给出此条目的代码位置细节;默认情况下,Massif只记录占有用内存字节1%以上的代码位置的详细信息,ms_print同样只打印占有用内存字节1%以上的代码位置的详细信息。不满足此阈值的条目将被聚合。这避免了用大量不重要的条目填充输出。阈值可以通过Massif和ms_print都支持的–threshold选项进行更改。
Forking Programs
如果程序分叉,子程序将继承为父程序收集的所有分析数据。
如果输出文件格式字符串(由–massif-out-file控制)不包含%p,那么来自父文件和子文件的输出将混合在一个输出文件中,这几乎肯定会使ms_print无法读取。
--massif-out-file=<file> [default: massif.out.%p]
将概要数据写入文件,而不是默认的输出文件mass .out.<pid>。%p和%q格式说明符可用于在名称中嵌入进程ID和/或环境变量的内容,就像核心选项–log-file一样。
测量一个进程中的所有内存
值得强调的是,默认情况下,Massif只度量堆内存,即使用malloc、calloc、realloc、memalign、new、new[]和其他一些类似函数分配的内存。(当然,它还可以选择测量堆栈内存。)这意味着它不会直接测量用mmap、mremap和brk等较低级别系统调用分配的内存。
堆分配函数(如malloc)构建在这些系统调用之上。例如,当需要时,分配器通常会调用mmap来分配一个大的内存块,然后将内存块的一部分交给客户机程序,以响应对malloc等的调用。Massif直接度量的只是这些较高级别的malloc等调用,而不是较低级别的系统调用。
此外,客户端程序可以直接使用这些较低级别的系统调用来分配内存。默认情况下,Massif不测量这些。它也不衡量代码、数据和BSS段的大小。因此,Massif报告的数据可能比top等测量程序总内存大小的工具报告的数据要小得多。
但是,如果希望测量程序使用的所有内存,可以使用–pages-as-heap=yes。当启用此选项时,Massif的普通堆块分析将被较低级别的页面分析所取代。通过mmap和类似的系统调用分配的每个页面都被视为不同的块。这意味着代码、数据和BSS段都是度量的,因为它们只是内存页。甚至堆栈也是度量的,因为它最终是通过mmap分配(并在必要时扩展)的;因此,不允许将–stacks=yes与–pages-as-heap=yes结合使用。
在使用–pages-as-heap=yes之后,ms_print的输出基本没有变化。不同之处在于,每个详细快照的开头都写着:
(page allocation syscalls) mmap/mremap/brk, --alloc-fns, etc.
instead of the usual:
(heap allocation functions) malloc/new/new[], --alloc-fns, etc.
输出中的堆栈跟踪可能比较难读,解释它们可能需要对较低级别的程序(如内存分配器)有一些详细的了解。但是对于某些程序来说,掌握关于内存使用情况的全部信息是非常有用的。
使用massif-visualizer
mass -visualizer是一个针对海量数据的图形化查看器,通常比ms_print更容易使用。mass -visualizer并没有在Valgrind内部提供,但可以在网上的各个地方获得。
安装:sudo apt install massif-visualizer
运行:massif-visualizer massif.out.xxx