Mesh: Efficient Memory Management for C/C++
Mesh: Efficient Memory Management for C/C++
Abstract integers, store flags in the low bits of aligned addresses, per-
Programs written in C/C++ can suffer from serious memory form arithmetic on addresses and later reference them, or
fragmentation, leading to low utilization of memory, de- even store addresses to disk and later reload them. This
graded performance, and application failure due to memory hostile environment makes it impossible to safely relocate
exhaustion. This paper introduces Mesh, a plug-in replace- objects: if an object is relocated, all pointers to its original
ment for malloc that, for the first time, eliminates fragmen- location must be updated. However, there is no way to safely
tation in unmodified C/C++ applications. Mesh combines update every reference when they are ambiguous, much less
novel randomized algorithms with widely-supported virtual when they are absent.
memory operations to provably reduce fragmentation, break- Existing memory allocators for C/C++ employ a variety of
ing the classical Robson bounds with high probability. Mesh best-effort heuristics aimed at reducing average fragmenta-
generally matches the runtime performance of state-of-the- tion [17]. However, these approaches are inherently limited.
art memory allocators while reducing memory consumption; In a classic result, Robson showed that all such allocators can
in particular, it reduces the memory of consumption of Fire- suffer from catastrophic memory fragmentation [26]. This
fox by 16% and Redis by 39%. increase in memory consumption can be as high as the log
of the ratio between the largest and smallest object sizes allo-
CCS Concepts • Software and its engineering → Allo- cated. For example, for an application that allocates 16-byte
cation / deallocation strategies; and 128KB objects, it is possible for it to consume 13× more
Keywords Memory management, runtime systems, un- memory than required.
managed languages Despite nearly fifty years of conventional wisdom indicat-
ing that compaction is impossible in unmanaged languages,
1 Introduction this paper shows that it is not only possible but also practical.
It introduces Mesh, a memory allocator that effectively and
Memory consumption is a serious concern across the spec- efficiently performs compacting memory management to
trum of modern computing platforms, from mobile to desk- reduce memory usage in unmodified C/C++ applications.
top to datacenters. For example, on low-end Android devices, Crucially and counterintuitively, Mesh performs com-
Google reports that more than 99 percent of Chrome crashes paction without relocation; that is, without changing the
are due to running out of memory when attempting to dis- addresses of objects. This property is vital for compatibility
play a web page [15]. On desktops, the Firefox web browser with arbitrary C/C++ applications. To achieve this, Mesh
has been the subject of a five-year effort to reduce its memory builds on a mechanism which we call meshing, first intro-
footprint [28]. In datacenters, developers implement a range duced by Novark et al.’s Hound memory leak detector [23].
of techniques from custom allocators to other ad hoc ap- Hound employed meshing in an effort to avoid catastrophic
proaches in an effort to increase memory utilization [25, 27]. memory consumption induced by its memory-inefficient allo-
A key challenge is that, unlike in garbage-collected en- cation scheme, which can only reclaim memory when every
vironments, automatically reducing a C/C++ application’s object on a page is freed. Hound first searches for pages
memory footprint via compaction is not possible. Because whose live objects do not overlap. It then copies the contents
the addresses of allocated objects are directly exposed to of one page onto the other, remaps one of the virtual pages
programmers, C/C++ applications can freely modify or hide to point to the single physical page now holding the contents
addresses. For example, a program may stash addresses in
Bobby Powers, David Tench, Emery D. Berger, and Andrew McGregor
free free
allocated allocated
munmap
(a) Before: these pages are candidates for “meshing” (b) After: both virtual pages now point to the first physical page;
because their allocated objects do not overlap. the second page is now freed.
Figure 1. Mesh in action. Mesh employs novel randomized algorithms that let it efficiently find and then “mesh” candidate
pages within spans (contiguous 4K pages) whose contents do not overlap. In this example, it increases memory utilization
across these pages from 37.5% to 75%, and returns one physical page to the OS (via munmap), reducing the overall memory
footprint. Mesh’s randomized allocation algorithm ensures meshing’s effectiveness with high probability.
of both pages, and finally relinquishes the other physical export LD_PRELOAD=[Link]). Our empirical evalua-
page to the OS. Figure 1 illustrates meshing in action. tion demonstrates that our implementation of Mesh is both
Mesh overcomes two key technical challenges of mesh- fast and efficient in practice. It generally matches the per-
ing that previously made it both inefficient and potentially formance of state-of-the-art allocators while guaranteeing
entirely ineffective. First, Hound’s search for pages to mesh the absence of catastrophic fragmentation with high prob-
involves a linear scan of pages on calls to free. While this ability. In addition, it occasionally yields substantial space
search is more efficient than a naive O(n 2 ) search of all possi- savings: replacing the standard allocator with Mesh auto-
ble pairs of pages, it remains prohibitively expensive for use matically reduces memory consumption by 16% (Firefox) to
in the context of a general-purpose allocator. Second, Hound 39% (Redis).
offers no guarantees that any pages would ever be meshable.
Consider an application that happens to allocate even one 1.1 Contributions
object in the same offset in every page. That layout would This paper makes the following contributions:
preclude meshing altogether, eliminating the possibility of
saving any space. • It introduces Mesh, a novel memory allocator that acts
Mesh makes meshing both efficient and provably effec- as a plug-in replacement for malloc. Mesh combines
tive (with high probability) by combining it with two novel remapping of virtual to physical pages (meshing) with
randomized algorithms. First, Mesh uses a space-efficient randomized allocation and search algorithms to enable
randomized allocation strategy that effectively scatters ob- safe and effective compaction without relocation for
jects within each virtual page, making the above scenario C/C++ (§2, §3, §4).
provably exceedingly unlikely. Second, Mesh incorporates • It presents theoretical results that guarantee Mesh’s
an efficient randomized algorithm that is guaranteed with efficiency and effectiveness with high probability (§5).
high probability to quickly find candidate pages that are • It evaluates Mesh’s performance empirically, demon-
likely to mesh. These two algorithms work in concert to en- strating Mesh’s ability to reduce space consumption
able formal guarantees on Mesh’s effectiveness. Our analysis while generally imposing low runtime overhead (§6).
shows that Mesh breaks the above-mentioned Robson worst
case bounds for fragmentation with high probability [26]. 2 Overview
We implement Mesh as a library for C/C++ applications This section provides a high-level overview of how Mesh
running on Linux of Mac OS X. Mesh interposes on memory works and gives some intuition as to how its algorithms
management operations, making it possible to use it without and implementation ensure its efficiency and effectiveness,
code changes or even recompilation by setting the appro- before diving into detailed description of Mesh’s algorithms
priate environment variable to load the Mesh library (e.g., (§3), implementation (§4), and its theoretical analysis (§5).
Mesh
SplitMesher(S, t) Mesh uses the same size classes correspond to those used by
1 n = length(S) jemalloc for objects 1024 bytes and smaller [11], and power-
2 Sl , Sr = S[1 : n/2], S[n/2 + 1 : n] of-two size classes for objects between 1024 and 16K. Alloca-
3 for (i = 0, i < t, i + +) tions are fulfilled from the smallest size class they fit in (e.g.,
4 len = |Sl | objects of size 33–48 bytes are served from the 48-byte size
5 for (j = 0, j < len, j + +) class); objects larger than 16K are individually fulfilled from
6 if Meshable (Sl (j), Sr (j + i % len)) the global arena. Small objects are allocated out of spans (§2),
7 Sl ← Sl \ Sl (j) which are multiples of the page size and contain between 8
8 Sr ← Sr \ Sr (j + i % len) and 256 objects of a fixed size. Having at least eight objects
9 mesh(Sl (j), Sr (j + i % len)) per span helps amortize the cost of reserving memory from
the global manager for the current thread’s allocator.
Figure 2. Meshing random pairs of spans. SplitMesher Objects of 4KB and larger are always page-aligned and
splits the randomly ordered span list S into halves, then span at least one entire page. Mesh does not consider these
probes pairs between halves for meshes. Each span is probed objects for meshing; instead, the pages are directly freed to
up to t times. the OS.
Mesh’s heap organization consists of four main compo-
the object as free, updates the span’s occupancy bin; this nents. MiniHeaps track occupancy and other metadata for
action may additionally trigger meshing. spans (§4.1). Shuffle vectors enable efficient, random allo-
cation out of a MiniHeap (§4.2). Thread local heaps satisfy
3.3 Meshing
small-object allocation requests without the need for locks
When meshing, Mesh randomly chooses pairs of spans and or atomic operations in the common case (§4.3). Finally,
attempts to mesh each pair. The meshing algorithm, which the global heap (§4.4) manages runtime state shared by all
we call SplitMesher (Figure 2), is designed both for practical threads, large object allocation, and coordinates meshing
effectiveness and for its theoretical guarantees. The parame- operations (§4.5).
ter t, which determines the maximum number of times each
span is probed (line 3), enables space-time trade-offs. The 4.1 MiniHeaps
parameter t can be increased to improve mesh quality and
therefore reduce space, or decreased to improve runtime, at MiniHeaps manage allocated physical spans of memory and
the cost of sacrificed meshing opportunities. We empirically are either attached or detached. An attached MiniHeap is
found that t = 64 balances runtime and meshing effective- owned by a specific thread-local heap, while a detached Mini-
ness, and use this value in our implementation. Heap is only referenced through the global heap. New small
SplitMesher proceeds by iterating through Sl and check- objects are only allocated out of attached MiniHeaps.
ing whether it can mesh each span with another span chosen Each MiniHeap contains metadata that comprises span
from Sr (line 6). If so, it removes these spans from their length, object size, allocation bitmap, and the start addresses
respective lists and meshes them (lines 7–9). SplitMesher of any virtual spans meshed to a unique physical span. The
repeats until it has checked t ∗ |Sl | pairs of spans; §4.5 de- number of objects that can be allocated from a MiniHeap
scribes the implementation of SplitMesher in detail. bitmap is objectCount = spanSize / objSize. The allocation
bitmap is initialized to objectCount zero bits.
4 Implementation When a MiniHeap is attached to a thread-local shuffle vec-
tor (§4.2), each offset that is unset in the MiniHeap’s bitmap
We implement Mesh as a drop-in replacement memory allo-
is added to the shuffle vector, with that bit now atomically
cator that implements meshing for single or multi-threaded
set to one in the bitmap. This approach is designed to allow
applications written in C/C++. Its current implementation
multiple threads to free objects which keeping most memory
work for 64-bit Linux and Mac OS X binaries. Mesh can be ex-
allocation operations local in the common case.
plicitly linked against by passing -lmesh to the linker at com-
When an object is freed and the free is non-local (§3.2),
pile time, or loaded dynamically by setting the LD_PRELOAD
the bit is reset. When a new MiniHeap is allocated, there
(Linux) or DYLD_INSERT_LIBRARIES (Mac OS X) environ-
is only one virtual span that points to the physical memory
ment variables to point to the Mesh library. When loaded,
it manages. After meshing, there may be multiple virtual
Mesh interposes on standard libc functions to replace all
spans pointing to the MiniHeap’s physical memory.
memory allocation functions.
Mesh combines traditional allocation strategies with
4.2 Shuffle Vectors
meshing to minimize heap usage. Like most modern memory
allocators [2, 3, 11, 13, 22], Mesh is a segregated-fit allocator. Shuffle vectors are a novel data structure that lets Mesh
Mesh employs fine-grained size classes to reduce internal perform randomized allocation out of a MiniHeap efficiently
fragmentation due to rounding up to the nearest size class. and with low space overhead.
Mesh
not immediately returned to the OS as they are likely to void *MeshLocal::malloc(size_t sz) {
be needed again soon, and reclamation is relatively expen- int szClass;
sive. Only after 64MB of used pages have accumulated, or // forward to global heap if large
whenever meshing is invoked, Mesh returns pages to OS by if (!getSizeClass(sz, &szClass))
calling fallocate on the heap’s file descriptor (§4.5.1) with return _global->malloc(sz);
the FALLOC_FL_PUNCH_HOLE flag. auto shufVec = _shufVecs[szClass];
if ([Link]()) {
4.4.2 MiniHeap allocation [Link]();
[Link](
Allocating a MiniHeap of size k pages begins with requesting
_global->allocMiniheap(szClass));}
k pages from the meshable arena. The global allocator then
return [Link]();
allocates and initializes a new MiniHeap instance from an }
internal allocator that Mesh uses for its own needs. This
MiniHeap is kept live so long as the number of allocated void ShuffleVector::attach(MiniHeap *mh){
objects remains non-zero, and singleton MiniHeaps are used _mh = mh;
to account for large object allocations. Finally, the global al- _off = maxCount();
locator updates the mapping of offsets to MiniHeaps for each for (auto i = 0; i < maxCount(); i++){
of the k pages to point at the address of the new MiniHeap. // true if atomically set (0 -> 1)
if ([Link](i)) {
4.4.3 Large objects _list[_off--] = i;
} }
All large allocation requests (greater than 16K) are directly
shuffle(_list[_off],
handled by the global heap. Large allocation requests are
_list[maxCount()]);
rounded up to the nearest multiple of the hardware page size }
(4K on x86_64), and a MiniHeap for 1 object of that size is
requested, as detailed above. The start of the span tracked void *ShuffleVector::malloc() {
by that MiniHeap is returned to the program as the result of const auto off = _list[_off++];
the malloc call. return _spanStart + off * _objSize;
}
4.4.4 Non-local frees
If free is called on a pointer that is not contained in an void MeshLocal::free(void *ptr) {
attached MiniHeap for that thread, the free is handled by // check if in attached MiniHeap
for (auto i=0; i<SizeClassCount; i++){
the global heap. Non-local frees occur when the thread that
const auto curr = _shufVecs[i];
frees the object is different from the thread that allocated
if (curr->contains(ptr)) {
it, or if there have been sufficient allocations on the current curr->free(ptr);
thread that the original MiniHeap was exhaused and a new return; } }
MiniHeap for that size class was attached. _global->free(ptr); // general case
Looking up the owning MiniHeap for a pointer is a con- }
stant time operation. The pointer is checked to ensure it falls
within the arena, the arena start address is subtracted from void ShuffleVector::free(void *ptr) {
it, and the result is divided by the page size. The resulting const auto freedOff = getOff(ptr);
offset is then used to index into a table of MiniHeap pointers. _list[--_off] = freedOff;
If the result is zero, the pointer is invalid (memory manage- // place newly freed address
// randomly in the shuffle vector
ment errors like double-frees are thus easily discovered and
auto swapOff =
discarded); otherwise, it points to a live MiniHeap.
_rng.inRange(_off, maxCount() - 1);
Once the owning MiniHeap has been found, that Mini- swap(_list[_off], _list[swapOff]);
Heap’s bitmap is updated atomically in a compare-and-set }
loop. If a free occurs for an object where the owning Mini-
Heap is attached to a different thread, the free atomically
updates that MiniHeap’s bitmap, but does not update the Figure 4. Pseudocode for Mesh’s core allocation and deallo-
other thread’s corresponding shuffle vector. cation routines.
4.5 Meshing Meshing is rate limited by a configurable parameter, set-
Mesh’s implementation of meshing is guided by theoretical table at program startup and during runtime by the applica-
results (described in detail in Section 5) that enable it to tion through the semi-standard mallctl API. The default
efficiently find a number of spans that can be meshed. rate meshes at most once every tenth of a second. If the last
Mesh
5.1 Formal Problem Definitions Rather than reason about MinCliqeCover on a meshing
Since Mesh segregates objects based on size, we can limit our graph G, we consider the equivalent problem of coloring
analysis to compaction within a single size class without loss the complement graph Ḡ in which there is an edge between
of generality. For our analysis, we represent spans as binary every pair of two nodes whose strings do not mesh. The
strings of length b, the maximum number of objects that the nodes of Ḡ can be partitioned into at most 2b − 1 subsets
span can store. Each bit represents the allocation state of a N 1 . . . N 2b −1 such that all nodes in each Ni represent the
single object. We represent each span π with string s such same string si . The induced subgraph of Ni in Ḡ is a clique
that s (i) = 1 if π has an object at offset i, and 0 otherwise. since all its nodes have a 1 in the same position and so cannot
be pairwise meshed. Further, all nodes in Ni have the same
Definition 5.1. We say two strings s 1 , s 2 mesh iff i s 1 (i) ·
Í
set of neighbors.
s 2 (i) = 0. More generally, a set of binary strings are said to Since Ni is a clique, at most one node in Ni may be col-
mesh if every pair of strings in this set mesh. ored with any color. Fix some coloring on Ḡ. Swapping the
When we mesh k spans together, the objects scattered colors of two nodes in Ni does not change the validity of the
across those k spans are moved to a single span while retain- coloring since these nodes have the same neighbor set. We
ing their offset from the start of the span. The remaining can therefore unambiguously represent a valid coloring of Ḡ
k − 1 spans are no longer needed and are released to the merely by indicating in which cliques each color appears.
operating system. We say that we “release” k − 1 strings With 2b cliques and a maximum of n colors, there are at
when we mesh k strings together. Since our goal is to empty most (n + 1)c such colorings on the graph where c = 22 .
b
as many physical spans as possible, we can characterize our This follows because each color used can be associated with
theoretical problem as follows: a subset of {1, . . . , 2b } corresponding to which of the cliques
Problem 1. Given a multi-set of n binary strings of length b, have node with this color; we call this subset a signature
find a meshing that releases the maximum number of strings. and note there are c possible signatures. A coloring can be
therefore be associated with a multi-set of possible signatures
Note that the total number of strings released is equal to where each signature has multiplicity between 0 and n; there
n − ρ − ϕ, where ρ is the number of total meshes performed, are (n + 1)c such multi-sets. This is polynomial in n since b
and ϕ is the number of strings that remain unmeshed. is constant and hence c is also constant. So we can simply
A Formulation via Graphs: We observe that an instance check each coloring for validity (a coloring is valid iff no
of the meshing problem, a string multi-set S, can naturally color appears in two cliques whose string representations
be expressed via a graph G(S) where there is a node for every mesh). The algorithm returns a valid coloring with the lowest
string in S and an edge between two nodes iff the relevant number of colors from all valid colorings discovered. □
strings can be meshed. Figure 5 illustrates this representation
Unfortunately, while technically polynomial, the running
via an example.
time of the above algorithm would obviously be prohibitive
If a set of strings are meshable, then there is an edge be-
in practice. Fortunately, as we show, we can exploit the
tween every pair of the corresponding nodes: the set of
randomness in the strings to design a much faster algorithm.
corresponding nodes is a clique. We can therefore decom-
pose the graph into k disjoint cliques iff we can free n − k 5.2 Simplifying the Problem: From
strings in the meshing problem. Unfortunately, the prob- MinCliqeCover to Matching
lem of decomposing a graph into the minimum number of
disjoint cliques (MinCliqeCover) is in general NP-hard. We leverage Mesh’s random allocation to simplify meshing;
Worse, it cannot even be approximated up to a factor m1−ϵ this random allocation implies a distribution over the graphs
unless P = N P [30]. that exhibits useful structural properties. We first make the
While the meshing problem is reducible to MinCliqe- following important observation:
Cover, we have not shown that the meshing problem is NP- Observation 1. Conditioned on the occupancies of the strings,
Hard. The meshing problem is indeed NP-hard for strings of edges in the meshing graph are not three-wise independent.
arbitrary length, but in practice string length is proportional
to span size, which is constant. To see that edges are not three-wise independent consider
three random strings s 1 , s 2 , s 3 of length 16, each with exactly 6
Theorem 5.2. The meshing problem for S, a multi-set of ones. It is impossible for these strings to all mesh mutually, so
strings of constant length, is in P. their mesh graph cannot be a triangle. Hence, if we know that
Proof. We assume without loss of generality that S does not s 1 and s 2 mesh, and that s 2 and s 3 mesh, we know for certain
contain the all-zero string s 0 ; if it does, since s 0 can be meshed that s 1 and s 3 cannot mesh. More generally, conditioning
with any other string and so can always be released, we can on s 1 and s 2 meshing and s 1 and s 3 meshing decreases the
solve the meshing problem for S \ s 0 and then mesh each probability that s 1 and s 3 mesh. Below, we quantify this effect
instance of s 0 arbitrarily. to argue that we can mesh near-optimally by solving the
Mesh
much easier Matching problem on the meshing graph (i.e., Lemma 5.3. If t = k/q for some user defined parameter k > 1,
restricting our attention to finding cliques of size 2) instead SplitMesher finds a matching of size at least n(1−e −2k )/4 be-
of MinCliqeCover. Another consequence of the above tween the left and right span sets with probability approaching
observation is that we will not be able to appeal to theoretical 1 as n ≥ 2k/q grows.
results on the standard model of random graphs, Erdős-Renyi
graphs, in which each possible edge is present with some Proof. Let Sl = {v 1 , v 2 , . . . vn/2 } and Sr = {u 1 , u 2 , . . . un/2 }.
fixed probability and the edges are fully independent. Instead Let t = k/q where k > 1 is some arbitrary constant. For
we will need new algorithms and proofs that only require ui ∈ Sl and i ≤ j ≤ j + t, we say (ui , v j ) is a good match if all
independence of acyclic collections of edges. the following properties hold: (1) there is an edge between ui
and v j , (2) there are no edges between ui and v j ′ for i ≤ j ′ < j,
Triangles and Larger Cliques are Uncommon. Because and (3) there are no edges between ui ′ and v j for i < i ′ ≤ j.
of the dependencies across the edges present in a meshing We observe that SplitMesher finds any good match, al-
graph, we can argue that triangles (and hence also larger though it may also find additional matches. It therefore
cliques) are relatively infrequent in the graph and certainly suffices to consider only the number of good matches. The
less frequent than one would expect were all edges indepen- probability (ui , v j ) is a good match is q(1 − q)2(j−i) by appeal-
dent. For example, consider three strings s 1 , s 2 , s 3 ∈ {0, 1}b ing to the fact that the collection of edges under consideration
with occupancies r 1 , r 2 , and r 3 , respectively. The probability is acyclic. Hence, Pr(ui has a good match) is
they mesh is kÕ
/q−1
1 − (1 − q)2k /q 1 − e −2k
r := q (1 − q)2i = q > .
1 − (1 − q) 2 2
b − r1 b b − r1 − r2 b i=0
× .
r2 r2 r3 r3 To analyze the number of good Í matches, define X i = 1
iff ui has a good match. Then, i X i is the number of good
This value is significantly less than would have been the matches. By linearity of expectation, the expected number
of good matches is rn/2. We decompose i X i into
Í
case if the events corresponding to pairs of strings being
meshable were independent. For instance, if b = 32, r 1 = Õ
r 2 = r 3 = 10, this probability is so low that even if there were Z 0 + Z 1 + . . . + Z t −1 where Z j = Xi .
1000 strings, the expected number of triangles would be less i≡j mod t
than 2. In contrast, had all meshes been independent, with
the same parameters, there would have been 167 triangles. Since each Z j is a sum of n/(2t) independent variables, by the
Chernoff bound, P Z j < (1 − ϵ) E[Z j ] ≤ exp −ϵ 2rn/(4t) .
The above analysis suggests that we can focus on finding
only cliques of size 2, thereby solving Matching instead of By the union bound,
MinCliqeCover. The evaluation in Section 6 vindicates
P (X < (1 − ϵ) rn/2) ≤ t exp −ϵ 2rn/(4t)
this approach, and we show a strong accuracy guarantee for
Matching below. and this becomes arbitrarily small as n grows. □
5.3 Theoretical Guarantees In the worst case, the algorithm checks nk/2q pairs. For
Since we need to perform meshing at runtime, it is essential our implementation of Mesh, we use a static value of t = 64;
that our algorithm for finding strings to mesh be as effi- this value enables the guarantees of Lemma 5.1 in cases
cient as possible. It would be far too costly in both time and where significant meshing is possible. As Section 6 shows,
memory overhead to actually construct the meshing graph this value for t results in effective memory compaction with
and run an existing matching algorithm on it. Instead, the modest performance overhead.
SplitMesher algorithm (shown in Figure 2) performs mesh-
ing without the need for explicitly constructing the meshing 5.4 Summary of Analytical Results
graph. We show the problem of meshing is reducible to a graph
For further efficiency, we need to constrain the value of the problem, MinCliqeCover. While solving this problem is
parameter t, which controls Mesh’s space-time tradeoff. If t infeasible, we show that probabilistically, we can do nearly
were set as large as n, then SplitMesher could, in the worst as well by finding the maximum Matching, a much eas-
case, exhaustively search all pairs of spans between the left ier graph problem. We analyze our meshing algorithm as
and right sets: a total of n 2 /4 probes. In practice, we want to an approximation to the maximum matching on a random
choose a significantly smaller value for t so that Mesh can meshing graph, and argue that it succeeds with high probabil-
always complete the meshing process quickly without the ity. As a corollary of these results, Mesh breaks the Robson
need to search all possible pairs of strings. bounds with high probability.
Bobby Powers, David Tench, Emery D. Berger, and Andrew McGregor
6 Evaluation 800
RSS (MiB)
600
Our evaluation answers the following questions: Does Mesh
400
reduce overall memory usage with reasonable performance
200
overhead? (§6.2) Does randomization provide empirical ben-
0
efits beyond its analytical guarantees? (§6.3) 0 50 100
Time Since Program Start (seconds)
6.1 Experimental Setup default jemalloc Mesh
We perform all experiments on a MacBook Pro with 16 GiB of Figure 6. Firefox: Mesh decreases mean heap size by 16%
RAM and an Intel i7-5600U, running Linux 4.18 and Ubuntu over the course of the Speedometer 2.0 benchmark compared
Bionic. We use glibc 2.26 and jemalloc 3.6.0 for SPEC2006, with the version of jemalloc bundled with Firefox, with less
Redis 4.0.2, and Ruby 2.5.1. Two builds of Firefox 57.0.4 were than a 1% change in the reported Speedometer score (§6.2.1).
compiled as release builds, one with its internal allocator dis-
abled to allow the use of alternate allocators via LD_PRELOAD.
SPEC was compiled with clang version 4.0 at the -O2 opti- run and calculate average memory usage recorded by mstat.
mization level, and Mesh was compiled with gcc 8 at the -O3 We tested both a standard release build of Firefox, along
optimization level and with link-time optimization (-flto). with a release build that did not bundle Mozilla’s fork of
Measuring memory usage: To accurately measure the jemalloc (hereafter referred to as mozjemalloc) and instead
memory usage of an application over time, we developed directly called malloc-related functions, with Mesh included
a Linux-based utility, mstat, that runs a program in a new via LD_PRELOAD. We report the average resident set size
memory control group [21]. mstat polls the resident-set size over the course of the benchmark and a 15 second cooldown
(RSS) and kernel memory usage statistics for all processes in period afterward, collecting three runs per allocator.
the control group at a constant frequency. This enables us to Mesh reduces the memory consumption of Firefox by 16%
account for the memory required for larger page tables (due compared to Firefox’s bundled jemalloc allocator. Mesh re-
to meshing) in our evaluation. We have verified that mstat quires 530 MB (σ = 22.4 MB) to complete the benchmark,
does not perturb performance results. while the Mozilla allocator needs 632 MB (σ = 25.3 MB).
This result shows that Mesh can effectively reduce mem-
6.2 Memory Savings and Performance Overhead ory overhead even in widely used and heavily optimized
We evaluate Mesh’s impact on memory consumption and applications. Mesh achieves this savings with less than a 1%
runtime across the Firefox web browser, the Redis data struc- reduction in performance (measured as the score reported
ture store, and the SPECint2006 benchmark suite. by Speedometer).
Figure 6 shows memory usage over the course of a
6.2.1 Firefox Speedometer benchmark run under Mesh and the default
Firefox is an especially challenging application for memory jemalloc allocator. While memory usage under both peaks
reduction since it has been the subject of a five year effort to similar levels, Mesh is able to keep heap size consistently
to reduce its memory footprint [28]. To evaluate Mesh’s lower.
impact on Firefox’s memory consumption under realistic
conditions, we measure Firefox’s RSS while running the 6.2.2 Redis
Speedometer 2.0 benchmark. Speedometer was constructed Redis is a widely-used in-memory data structure server.
by engineers working on the Google Chrome and Apple Sa- Redis 4.0 introduced a feature called “active defragmenta-
fari web browsers to simulate the patterns in use on websites tion” [25, 27]. Redis calculates a fragmentation ratio (RSS
today, stressing a number of browser subsystems like DOM over sum of active allocations) once a second. If this ratio
APIs, layout, CSS resolution and the JavaScript engine. In is too high, it triggers a round of active defragmentation.
Firefox, most of these subsystems are multi-threaded, even This involves making a fresh copy of Redis’s internal data
for a single page [10]. The benchmark comprises a number of structures and freeing the old ones. Active defragmentation
small “todo” apps written in a number of different languages relies on allocator-specific APIs in jemalloc both for gather-
and styles, with a final score computed as the geometric ing statistics and for its ability to perform allocations that
mean of the time taken by the executed tests. bypass thread-local caches, increasing the likelihood they
We test Firefox in single-process mode (disabling content will be contiguous in memory.
sandboxing, which spawns multiple processes) under the We adapt a benchmark from the official Redis test suite
mstat tool to record memory usage over time. Our test to measure how Mesh’s automatic compaction compares
opens a tab, loads the Speedometer page from a local server, with Redis’s active defragmentation, as well as against the
waits 2 seconds, and then automatically executes the test. standard glibc allocator. This benchmark runs for a total of
We record the reported score at the end of the benchmark 7.5 seconds, regardless of allocator. It configures Redis to act
Mesh
300 300
RSS (MiB)
RSS (MiB)
200 200
100 100
0 0
0 1 2 3 4 5 6 0 1 2 3
Time Since Program Start (seconds) Time Since Program Start (seconds)
jemalloc + activedefrag Mesh Mesh (no meshing) jemalloc Mesh Mesh (no meshing) Mesh (no rand)
Figure 7. Redis: Mesh automatically achieves significant Figure 8. Ruby benchmark: Mesh is able to decrease mean
memory savings (39%), obviating the need for its custom, heap size by 18% compared to Mesh with randomization
application-specific “defragmentation” routine (§6.2.2). disabled and non-compacting allocators (§6.3).
as an LRU cache with a maximum of 100 MB of objects (keys benchmark is [Link], a Perl benchmark that per-
and values). The test then allocates 700,000 random keys forms a number of e-mail related tasks including spam de-
and values, where the values have a length of 240 bytes. Fi- tection (SpamAssassin). With glibc, its peak RSS is 664MB.
nally, the test inserts 170,000 new keys with values of length Mesh reduces its peak RSS to 564MB (a 15% reduction) while
492. Our only change from the original Redis test is to in- increasing its runtime overhead by only 3.9%.
crease the value sizes in order to place all allocators on a
level playing field with respect to internal fragmentation; 6.3 Empirical Value of Randomization
the chosen values of 240 and 492 bytes ensure that tested Randomization is key to Mesh’s analytic guarantees; we
allocators use similar size classes for their allocations. We evaluate whether it also can have an observable empirical
test Mesh with Redis in two configurations: with meshing al- impact on its ability to reclaim space. To do this, we test three
ways on and with meshing disabled, both without any input configurations of Mesh: (1) meshing disabled, (2) meshing
or coordination from the redis-server application. enabled but randomization disabled, and (3) Mesh with both
Figure 7 shows memory usage over time for Redis under meshing and randomization enabled (the default).
Mesh, as well as under jemalloc with Redis’s “activedefrag” We tested these configurations with Firefox and Redis,
enabled, as measured by mstat (§6.1). The “activedefrag” and found no significant differences when randomization
configuration enables active defragmentation after all objects was disabled; we believe that this is due to the highly ir-
have been added to the cache. regular (effectively random) allocation patterns that these
Using Mesh automatically and portably achieves the same applications exhibit. We hypothesized that a more regu-
heap size reduction (39%) as Redis’s active defragmentation. lar allocation pattern would be more challenging for a non-
During most of the 7.5s of this test Redis is idle; Redis only randomized baseline. To test this hypothesis, we wrote a
triggers active defragmentation during idle periods. With synthetic microbenchmark with a regular allocation pattern
Mesh, insertion takes 1.76s, while with Redis’s default of in Ruby. Ruby is an interpreted programming language popu-
jemalloc, insertion takes 1.72s. Mesh’s compaction is addi- lar for implementing web services, including GitHub, AirBnB,
tionally significantly faster than Redis’s active defragmenta- and the original version of Twitter. Ruby makes heavy use
tion. During execution with Mesh, a total of 0.23s are spent of object-oriented and functional programming paradigms,
meshing (the longest pause is 22 ms), while active defrag- making it allocation-intensive. Ruby is garbage collected,
mentation accounts for 1.49s (5.5× slower). This high latency and while the standard MRI Ruby implementation (written
may explain why Redis disables “activedefrag” by default. in C) has a custom GC arena for small objects, large objects
(like strings) are allocated directly with malloc.
Our Ruby microbenchmark repeatedly performs a se-
6.2.3 SPEC Benchmarks
quence of string allocations and deallocations, simulating
Most of the SPEC benchmarks are not particularly com- the effect of accumulating results from an API and periodi-
pelling targets for Mesh because they have small overall cally filtering some out. It allocates a number of strings of a
footprints and do not exercise the memory allocator. Across fixed size, then retaining references 25% of the strings while
the entire SPECint 2006 benchmark suite, Mesh modestly dropping references to the rest. Each iteration the length of
decreases average memory consumption (geomean: −2.4%) the strings is doubled. The test requires only a fixed 128 MB
vs. glibc, while imposing minimal execution time overhead to hold the string contents.
(geomean: 0.7%). Figure 8 presents the results of running this application
However, for allocation-intensive applications with large with the three variants of Mesh and jemalloc; for this bench-
footprints, Mesh is able to substantially reduce peak memory mark, jemalloc and glibc are essentially indistinguishable.
consumption. In particular, the most allocation-intensive With meshing disabled, Mesh exhibits similar runtime and
Bobby Powers, David Tench, Emery D. Berger, and Andrew McGregor
heap size to jemalloc. With meshing enabled but random- not require programmer or compiler support; its compaction
ization disabled, Mesh imposes a 4% runtime overhead and approach is orthogonal to GC.
yields only a modest 3% reduction in heap size. CouchDB and Redis implement ad hoc best-effort com-
Enabling randomization in Mesh increases the time over- paction, which they call “defragmentation”. These work by
head to 10.7% compared to jemalloc, but the use of random- iterating through program data structures like hash tables,
ization lets it significantly reduce the mean heap size over copying each object’s contents into freshly-allocated blocks
the execution time of the microbenchmark (a 19% reduction). (in the hope they will be contiguous), updating pointers,
The additional runtime overhead is due to the additional and then freeing the old objects [25, 27]. This application-
system calls and memory copies induced by the meshing specific approach is not only inefficient (because it may copy
process. This result demonstrates that randomization is not objects that are already densely packed) and brittle (because
just useful for providing analytical guarantees but can also it relies on internal allocator behavior that may change in
be essential for meshing to be effective in practice. new releases), but it may also be ineffective, since the alloca-
tor cannot ensure that these objects are actually contiguous
6.4 Summary of Empirical Results in memory. Unlike these approaches, Mesh performs com-
For a number of memory-intensive applications, including paction efficiently and its effectiveness is guaranteed.
aggressively space-optimized applications like Firefox, Mesh
Compacting garbage collection in managed languages:
can substantially reduce memory consumption (by 16% to
Compacting garbage collection has long been a feature of
39%) while imposing a modest impact on runtime perfor-
languages like LISP and Java [12, 14]. Contemporary run-
mance (e.g., around 1% for Firefox and SPECint 2006). We
times like the Hotspot JVM [20], the .NET VM [19], and the
find that Mesh’s randomization can enable substantial space
SpiderMonkey JavaScript VM [7] all implement compaction
reduction in the face of a regular allocation pattern.
as part of their garbage collection algorithms. Mesh brings
the benefits of compaction to C/C++; in principle, it could
7 Related Work
also be used to automatically enable compaction for language
Hound: Hound is a memory leak detector for C/C++ applica- implementations that rely on non-compacting collectors.
tions that introduced meshing (a.k.a. “virtual compaction”),
a mechanism that Mesh leverages [23]. Hound combines an Bounds on Partial Compaction: Cohen and Petrank prove
age-segregated heap with data sampling to precisely iden- upper and lower bounds on defragmentation via partial com-
tify leaks. Because Hound cannot reclaim memory until paction [5, 6]. In their setting, corresponding to managed en-
every object on a page is freed, it relies on a heuristic version vironments, every object may be relocated to any free mem-
of meshing to prevent catastrophic memory consumption. ory location; they ask what space savings can be achieved if
Hound is unsuitable as a replacement general-purpose allo- the memory manager is only allowed to relocate a bounded
cator; it lacks both Mesh’s theoretical guarantees and space number of objects. By contrast, Mesh is designed for unman-
and runtime efficiency (Hound’s repository is missing files aged languages where objects cannot be arbitrarily relocated.
and it does not build, precluding a direct empirical com-
PCM fault mitigation: Ipek et al. use a technique similar
parison here). The Hound paper reports a geometric mean
to meshing to address the degradation of phase-change mem-
slowdown of ≈ 30% for SPECint2006 (compared to Mesh’s
ory (PCM) over the lifetime of a device [16]. The authors
0.7%), slowing one benchmark (xalancbmk) by almost 10×.
introduce dynamically replicated memory (DRM), which
Hound also generally increases memory consumption, while
uses pairs of PCM pages with non-overlapping bit failures
Mesh often substantially decreases it.
to act as a single page of (non-faulty) storage. When the
Compaction for C/C++: Previous work has described a va- memory controller reports a page with new bit failures, the
riety of manual and compiler-based approaches to support OS attempts to pair it with a complementary page. A random
compaction for C++. Detlefs shows that if developers use graph analysis is used to justify this greedy algorithm.
annotations in the form of smart pointers, C++ code can also DRM operates in a qualitatively different domain than
be managed with a relocating garbage collector [8]. Edelson Mesh. In DRM, the OS occasionally attempts to pair newly
introduced GC support through a combination of automati- faulty pages against a list of pages with static bit failures.
cally generated smart pointer classes and compiler transfor- This process is incremental and local. In Mesh, the occu-
mations that support relocating GC [9]. Google’s Chrome pancy of spans in the heap is more dynamic and much less
uses an application-specific compacting GC for C++ objects local. Mesh solves a full, non-incremental version of the
called Oilpan that depends on the presence of a single event meshing problem each cycle. Additionally, in DRM, the ran-
loop [1]. Developers must use a variety of smart pointer dom graph describes an error model rather than a design
classes instead of raw pointers to enable GC and relocation. decision; additionally, the paper’s analysis is flawed. The
This effort took years. Unlike these approaches, Mesh is fully paper erroneously claims that the resulting graph is a sim-
general, works for unmodified C and C++ binaries, and does ple random graph; in fact, its edges are not independent (as
Mesh
[30] David Zuckerman. 2007. Linear Degree Extractors and the Inapprox-
imability of Max Clique and Chromatic Number. Theory of Computing
3, 6 (2007), 103–128.