1 1.1 christos # jemalloc profiling 2 1.1 christos This describes the mathematical basis behind jemalloc's profiling implementation, as well as the implementation tricks that make it effective. Historically, the jemalloc profiling design simply copied tcmalloc's. The implementation has since diverged, due to both the desire to record additional information, and to correct some biasing bugs. 3 1.1 christos 4 1.1 christos Note: this document is markdown with embedded LaTeX; different markdown renderers may not produce the expected output. Viewing with `pandoc -s PROFILING_INTERNALS.md -o PROFILING_INTERNALS.pdf` is recommended. 5 1.1 christos 6 1.1 christos ## Some tricks in our implementation toolbag 7 1.1 christos 8 1.1 christos ### Sampling 9 1.1 christos Recording our metadata is quite expensive; we need to walk up the stack to get a stack trace. On top of that, we need to allocate storage to record that stack trace, and stick it somewhere where a profile-dumping call can find it. That call might happen on another thread, so we'll probably need to take a lock to do so. These costs are quite large compared to the average cost of an allocation. To manage this, we'll only sample some fraction of allocations. This will miss some of them, so our data will be incomplete, but we'll try to make up for it. We can tune our sampling rate to balance accuracy and performance. 10 1.1 christos 11 1.1 christos ### Fast Bernoulli sampling 12 1.1 christos Compared to our fast paths, even a `coinflip(p)` function can be quite expensive. Having to do a random-number generation and some floating point operations would be a sizeable relative cost. However (as pointed out in [[Vitter, 1987](https://dl.acm.org/doi/10.1145/23002.23003)]), if we can orchestrate our algorithm so that many of our `coinflip` calls share their parameter value, we can do better. We can sample from the geometric distribution, and initialize a counter with the result. When the counter hits 0, the `coinflip` function returns true (and reinitializes its internal counter). 13 1.1 christos This can let us do a random-number generation once per (logical) coinflip that comes up heads, rather than once per (logical) coinflip. Since we expect to sample relatively rarely, this can be a large win. 14 1.1 christos 15 1.1 christos ### Fast-path / slow-path thinking 16 1.1 christos Most programs have a skewed distribution of allocations. Smaller allocations are much more frequent than large ones, but shorter lived and less common as a fraction of program memory. "Small" and "large" are necessarily sort of fuzzy terms, but if we define "small" as "allocations jemalloc puts into slabs" and "large" as the others, then it's not uncommon for small allocations to be hundreds of times more frequent than large ones, but take up around half the amount of heap space as large ones. Moreover, small allocations tend to be much cheaper than large ones (often by a factor of 20-30): they're more likely to hit in thread caches, less likely to have to do an mmap, and cheaper to fill (by the user) once the allocation has been returned. 17 1.1 christos 18 1.1 christos ## An unbiased estimator of space consumption from (almost) arbitrary sampling strategies 19 1.1 christos Suppose we have a sampling strategy that meets the following criteria: 20 1.1 christos 21 1.1 christos - One allocation being sampled is independent of other allocations being sampled. 22 1.1 christos - Each allocation has a non-zero probability of being sampled. 23 1.1 christos 24 1.1 christos We can then estimate the bytes in live allocations through some particular stack trace as: 25 1.1 christos 26 1.1 christos $$ \sum_i S_i I_i \frac{1}{\mathrm{E}[I_i]} $$ 27 1.1 christos 28 1.1 christos where the sum ranges over some index variable of live allocations from that stack, $S_i$ is the size of the $i$'th allocation, and $I_i$ is an indicator random variable for whether or not the $i'th$ allocation is sampled. $S_i$ and $\mathrm{E}[I_i]$ are constants (the program allocations are fixed; the random variables are the sampling decisions), so taking the expectation we get 29 1.1 christos 30 1.1 christos $$ \sum_i S_i \mathrm{E}[I_i] \frac{1}{\mathrm{E}[I_i]}.$$ 31 1.1 christos 32 1.1 christos This is of course $\sum_i S_i$, as we want (and, a similar calculation could be done for allocation counts as well). 33 1.1 christos This is a fairly general strategy; note that while we require that sampling decisions be independent of one another's outcomes, they don't have to be independent of previous allocations, total bytes allocated, etc. You can imagine strategies that: 34 1.1 christos 35 1.1 christos - Sample allocations at program startup at a higher rate than subsequent allocations 36 1.1 christos - Sample even-indexed allocations more frequently than odd-indexed ones (so long as no allocation has zero sampling probability) 37 1.1 christos - Let threads declare themselves as high-sampling-priority, and sample their allocations at an increased rate. 38 1.1 christos 39 1.1 christos These can all be fit into this framework to give an unbiased estimator. 40 1.1 christos 41 1.1 christos ## Evaluating sampling strategies 42 1.1 christos Not all strategies for picking allocations to sample are equally good, of course. Among unbiased estimators, the lower the variance, the lower the mean squared error. Using the estimator above, the variance is: 43 1.1 christos 44 1.1 christos $$ 45 1.1 christos \begin{aligned} 46 1.1 christos & \mathrm{Var}[\sum_i S_i I_i \frac{1}{\mathrm{E}[I_i]}] \\ 47 1.1 christos =& \sum_i \mathrm{Var}[S_i I_i \frac{1}{\mathrm{E}[I_i]}] \\ 48 1.1 christos =& \sum_i \frac{S_i^2}{\mathrm{E}[I_i]^2} \mathrm{Var}[I_i] \\ 49 1.1 christos =& \sum_i \frac{S_i^2}{\mathrm{E}[I_i]^2} \mathrm{Var}[I_i] \\ 50 1.1 christos =& \sum_i \frac{S_i^2}{\mathrm{E}[I_i]^2} \mathrm{E}[I_i](1 - \mathrm{E}[I_i]) \\ 51 1.1 christos =& \sum_i S_i^2 \frac{1 - \mathrm{E}[I_i]}{\mathrm{E}[I_i]}. 52 1.1 christos \end{aligned} 53 1.1 christos $$ 54 1.1 christos 55 1.1 christos We can use this formula to compare various strategy choices. All else being equal, lower-variance strategies are better. 56 1.1 christos 57 1.1 christos ## Possible sampling strategies 58 1.1 christos Because of the desire to avoid the fast-path costs, we'd like to use our Bernoulli trick if possible. There are two obvious counters to use: a coinflip per allocation, and a coinflip per byte allocated. 59 1.1 christos 60 1.1 christos ### Bernoulli sampling per-allocation 61 1.1 christos An obvious strategy is to pick some large $N$, and give each allocation a $1/N$ chance of being sampled. This would let us use our Bernoulli-via-Geometric trick. Using the formula from above, we can compute the variance as: 62 1.1 christos 63 1.1 christos $$ \sum_i S_i^2 \frac{1 - \frac{1}{N}}{\frac{1}{N}} = (N-1) \sum_i S_i^2.$$ 64 1.1 christos 65 1.1 christos That is, an allocation of size $Z$ contributes a term of $(N-1)Z^2$ to the variance. 66 1.1 christos 67 1.1 christos ### Bernoulli sampling per-byte 68 1.1 christos Another option we have is to pick some rate $R$, and give each byte a $1/R$ chance of being picked for sampling (at which point we would sample its contained allocation). The chance of an allocation of size $Z$ being sampled, then, is 69 1.1 christos 70 1.1 christos $$1-(1-\frac{1}{R})^{Z}$$ 71 1.1 christos 72 1.1 christos and an allocation of size $Z$ contributes a term of 73 1.1 christos 74 1.1 christos $$Z^2 \frac{(1-\frac{1}{R})^{Z}}{1-(1-\frac{1}{R})^{Z}}.$$ 75 1.1 christos 76 1.1 christos In practical settings, $R$ is large, and so this is well-approximated by 77 1.1 christos 78 1.1 christos $$Z^2 \frac{e^{-Z/R}}{1 - e^{-Z/R}} .$$ 79 1.1 christos 80 1.1 christos Just to get a sense of the dynamics here, let's look at the behavior for various values of $Z$. When $Z$ is small relative to $R$, we can use $e^z \approx 1 + x$, and conclude that the variance contributed by a small-$Z$ allocation is around 81 1.1 christos 82 1.1 christos $$Z^2 \frac{1-Z/R}{Z/R} \approx RZ.$$ 83 1.1 christos 84 1.1 christos When $Z$ is comparable to $R$, the variance term is near $Z^2$ (we have $\frac{e^{-Z/R}}{1 - e^{-Z/R}} = 1$ when $Z/R = \ln 2 \approx 0.693$). When $Z$ is large relative to $R$, the variance term goes to zero. 85 1.1 christos 86 1.1 christos ## Picking a sampling strategy 87 1.1 christos The fast-path/slow-path dynamics of allocation patterns point us towards the per-byte sampling approach: 88 1.1 christos 89 1.1 christos - The quadratic increase in variance per allocation in the first approach is quite costly when heaps have a non-negligible portion of their bytes in those allocations, which is practically often the case. 90 1.1 christos - The Bernoulli-per-byte approach shifts more of its samples towards large allocations, which are already a slow-path. 91 1.1 christos - We drive several tickers (e.g. tcache gc) by bytes allocated, and report bytes-allocated as a user-visible statistic, so we have to do all the necessary bookkeeping anyways. 92 1.1 christos 93 1.1 christos Indeed, this is the approach we use in jemalloc. Our heap dumps record the size of the allocation and the sampling rate $R$, and jeprof unbiases by dividing by $1 - e^{-Z/R}$. The framework above would suggest dividing by $1-(1-1/R)^Z$; instead, we use the fact that $R$ is large in practical situations, and so $e^{-Z/R}$ is a good approximation (and faster to compute). (Equivalently, we may also see this as the factor that falls out from viewing sampling as a Poisson process directly). 94 1.1 christos 95 1.1 christos ## Consequences for heap dump consumers 96 1.1 christos Using this approach means that there are a few things users need to be aware of. 97 1.1 christos 98 1.1 christos ### Stack counts are not proportional to allocation frequencies 99 1.1 christos If one stack appears twice as often as another, this by itself does not imply that it allocates twice as often. Consider the case in which there are only two types of allocating call stacks in a program. Stack A allocates 8 bytes, and occurs a million times in a program. Stack B allocates 8 MB, and occurs just once in a program. If our sampling rate $R$ is about 1MB, we expect stack A to show up about 8 times, and stack B to show up once. Stack A isn't 8 times more frequent than stack B, though; it's a million times more frequent. 100 1.1 christos 101 1.1 christos ### Aggregation must be done after unbiasing samples 102 1.1 christos Some tools manually parse heap dump output, and aggregate across stacks (or across program runs) to provide wider-scale data analyses. When doing this aggregation, though, it's important to unbias-and-then-sum, rather than sum-and-then-unbias. Reusing our example from the previous section: suppose we collect heap dumps of the program from a million machines. We then have 8 million occurs of stack A (each of 8 bytes), and a million occurrences of stack B (each of 8 MB). If we sum first, we'll attribute 64 MB to stack A, and 8 TB to stack B. Unbiasing changes these numbers by an infinitesimal amount, so that sum-then-unbias dramatically underreports the amount of memory allocated by stack A. 103 1.1 christos 104 1.1 christos ## An avenue for future exploration 105 1.1 christos While the framework we laid out above is pretty general, as an engineering decision we're only interested in fairly simple approaches (i.e. ones for which the chance of an allocation being sampled depends only on its size). Our job is then: for each size class $Z$, pick a probability $p_Z$ that an allocation of that size will be sampled. We made some handwave-y references to statistical distributions to justify our choices, but there's no reason we need to pick them that way. Any set of non-zero probabilities is a valid choice. 106 1.1 christos The real limiting factor in our ability to reduce estimator variance is that fact that sampling is expensive; we want to make sure we only do it on a small fraction of allocations. Our goal, then, is to pick the $p_Z$ to minimize variance given some maximum sampling rate $P$. If we define $a_Z$ to be the fraction of allocations of size $Z$, and $l_Z$ to be the fraction of allocations of size $Z$ still alive at the time of a heap dump, then we can phrase this as an optimization problem over the choices of $p_Z$: 107 1.1 christos 108 1.1 christos Minimize 109 1.1 christos 110 1.1 christos $$ \sum_Z Z^2 l_Z \frac{1-p_Z}{p_Z} $$ 111 1.1 christos 112 1.1 christos subject to 113 1.1 christos 114 1.1 christos $$ \sum_Z a_Z p_Z \leq P $$ 115 1.1 christos 116 1.1 christos Ignoring a term that doesn't depend on $p_Z$, the objective is minimized whenever 117 1.1 christos 118 1.1 christos $$ \sum_Z Z^2 l_Z \frac{1}{p_Z} $$ 119 1.1 christos 120 1.1 christos is. For a particular program, $l_Z$ and $a_Z$ are just numbers that can be obtained (exactly) from existing stats introspection facilities, and we have a fairly tractable convex optimization problem (it can be framed as a second-order cone program). It would be interesting to evaluate, for various common allocation patterns, how well our current strategy adapts. Do our actual choices for $p_Z$ closely correspond to the optimal ones? How close is the variance of our choices to the variance of the optimal strategy? 121 1.1 christos You can imagine an implementation that actually goes all the way, and makes $p_Z$ selections a tuning parameter. I don't think this is a good use of development time for the foreseeable future; but I do wonder about the answers to some of these questions. 122 1.1 christos 123 1.1 christos ## Implementation realities 124 1.1 christos 125 1.1 christos The nice story above is at least partially a lie. Initially, jeprof (copying its logic from pprof) had the sum-then-unbias error described above. The current version of jemalloc does the unbiasing step on a per-allocation basis internally, so that we're always tracking what the unbiased numbers "should" be. The problem is, actually surfacing those unbiased numbers would require a breaking change to jeprof (and the various already-deployed tools that have copied its logic). Instead, we use a little bit more trickery. Since we know at dump time the numbers we want jeprof to report, we simply choose the values we'll output so that the jeprof numbers will match the true numbers. The math is described in `src/prof_data.c` (where the only cleverness is a change of variables that lets the exponentials fall out). 126 1.1 christos 127 1.1 christos This has the effect of making the output of jeprof (and related tools) correct, while making its inputs incorrect. This can be annoying to human readers of raw profiling dump output. 128