- publishing free software manuals
Valgrind 3.3 - Advanced Debugging and Profiling for GNU/Linux applications
by J. Seward, N. Nethercote, J. Weidendorfer and the Valgrind Development Team
Paperback (6"x9"), 164 pages
ISBN 0954612051
RRP £12.95 ($19.95)

Get a printed copy>>>

8.2.4 The Snapshot Details

Returning to our example, the graph is followed by the detailed information for each snapshot. The first nine snapshots are normal, so only a small amount of information is recorded for each one:

-------------------------------------------------------
  n      time       total useful-heap extra-heap stacks
           (B)         (B)         (B)      (B)     (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

Each normal snapshot records several things.

The next snapshot is detailed. As well as the basic counts, it gives an allocation tree which indicates exactly which pieces of code were responsible for allocating heap memory:

  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)

The allocation tree can be read from the top down. The first line indicates all heap allocation functions such as malloc and C++ new. All heap allocations go through these functions, and so all 9,000 useful bytes (which is 99.21% of all allocated bytes) go through them. But how were malloc and new called? At this point, every allocation so far has been due to line 21 inside main, hence the second line in the tree. The ‘->’ indicates that main (line 20) called malloc.

Let's see what the subsequent output shows happened next:

-------------------------------------------------------
  n      time       total useful-heap extra-heap stacks
           (B)         (B)         (B)      (B)     (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)

The first four snapshots are similar to the previous ones. But then the global allocation peak is reached, and a detailed snapshot is taken. Its allocation tree shows that 20,000B of useful heap memory has been allocated, and the lines and arrows indicate that this is from three different code locations: line 20, which is responsible for 10,000B (49.74%); line 5, which is responsible for 8,000B (39.79%); and line 10, which is responsible for 2,000B (9.95%).

We can then drill down further in the allocation tree. For example, of the 8,000B asked for by line 5, half of it was due to a call from line 11, and half was due to a call from line 25.

In short, Massif collates the stack trace of every single allocation point in the program into a single tree, which gives a complete picture of how and why all heap memory was allocated.

Note that the tree entries correspond not to functions, but to individual code locations. For example, if function A calls malloc, and function B calls A twice, once on line 10 and once on line 11, then the two calls will result in two distinct stack traces in the tree. In contrast, if B calls A repeatedly from line 15 (e.g. due to a loop), then each of those calls will be represented by the same stack trace in the tree.

Note also that tree entry with children in the example satisfies an invariant: the entry's size is equal to the sum of its children's sizes. For example, the first entry has size 20,000B, and its children have sizes 10,000B, 8,000B, and 2,000B. In general, this invariant almost always holds. However, in rare circumstances stack traces can be malformed, in which case a stack trace can be a sub-trace of another stack trace. This means that some entries in the tree may not satisfy the invariant--the entry's size will be greater than the sum of its children's sizes. Massif can sometimes detect when this happens; if it does, it issues a warning:

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.

However, Massif does not detect and warn about every such occurrence. Fortunately, malformed stack traces are rare in practice.

Returning now to ms_print's output, the final part is similar:

-------------------------------------------------------
  n      time       total useful-heap extra-heap stacks
           (B)         (B)         (B)      (B)     (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%)

The final detailed snapshot shows how the heap looked at termination. The 00.00% entry represents the code locations for which memory was allocated and then freed (line 20 in this case, the memory for which was freed on line 28). However, no code location details are given for this entry; by default, Massif only records the details for code locations responsible for more than 1% of useful memory bytes, and ms_print likewise only prints the details for code locations responsible for more than 1%. The entries that do not meet this threshold are aggregated. This avoids filling up the output with large numbers of unimportant entries. The thresholds threshold can be changed with the ‘--threshold’ option that both Massif and ms_print support.

ISBN 0954612051Valgrind 3.3 - Advanced Debugging and Profiling for GNU/Linux applicationsSee the print edition