~ubuntu-branches/ubuntu/hardy/libgc/hardy-updates

« back to all changes in this revision

Viewing changes to doc/scale.html

  • Committer: Bazaar Package Importer
  • Author(s): Ryan Murray
  • Date: 2005-02-03 00:50:53 UTC
  • mto: (3.1.1 etch) (1.2.4 upstream)
  • mto: This revision was merged to the branch mainline in revision 3.
  • Revision ID: james.westby@ubuntu.com-20050203005053-9c0v9r2qcm2g1cfp
Tags: upstream-6.4
ImportĀ upstreamĀ versionĀ 6.4

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<HTML>
 
2
<HEAD>
 
3
<TITLE>Garbage collector scalability</TITLE>
 
4
</HEAD>
 
5
<BODY>
 
6
<H1>Garbage collector scalability</h1>
 
7
In its default configuration, the Boehm-Demers-Weiser garbage collector
 
8
is not thread-safe.  It can be made thread-safe for a number of environments
 
9
by building the collector with the appropriate
 
10
<TT>-D</tt><I>XXX</i><TT>-THREADS</tt> compilation
 
11
flag.  This has primarily two effects:
 
12
<OL>
 
13
<LI> It causes the garbage collector to stop all other threads when
 
14
it needs to see a consistent memory state.
 
15
<LI> It causes the collector to acquire a lock around essentially all
 
16
allocation and garbage collection activity.
 
17
</ol>
 
18
Since a single lock is used for all allocation-related activity, only one
 
19
thread can be allocating or collecting at one point.  This inherently
 
20
limits performance of multi-threaded applications on multiprocessors.
 
21
<P>
 
22
On most platforms, the allocator/collector lock is implemented as a
 
23
spin lock with exponential back-off.  Longer wait times are implemented
 
24
by yielding and/or sleeping.  If a collection is in progress, the pure
 
25
spinning stage is skipped.  This has the advantage that uncontested and
 
26
thus most uniprocessor lock acquisitions are very cheap.  It has the
 
27
disadvantage that the application may sleep for small periods of time
 
28
even when there is work to be done.  And threads may be unnecessarily
 
29
woken up for short periods.  Nonetheless, this scheme empirically
 
30
outperforms native queue-based mutual exclusion implementations in most
 
31
cases, sometimes drastically so.
 
32
<H2>Options for enhanced scalability</h2>
 
33
Version 6.0 of the collector adds two facilities to enhance collector
 
34
scalability on multiprocessors.  As of 6.0alpha1, these are supported 
 
35
only under Linux on X86 and IA64 processors, though ports to other
 
36
otherwise supported Pthreads platforms should be straightforward.
 
37
They are intended to be used together.
 
38
<UL>
 
39
<LI>
 
40
Building the collector with <TT>-DPARALLEL_MARK</tt> allows the collector to
 
41
run the mark phase in parallel in multiple threads, and thus on multiple
 
42
processors.  The mark phase typically consumes the large majority of the
 
43
collection time.  Thus this largely parallelizes the garbage collector
 
44
itself, though not the allocation process.  Currently the marking is
 
45
performed by the thread that triggered the collection, together with
 
46
<I>N</i>-1 dedicated
 
47
threads, where <I>N</i> is the number of processors detected by the collector.
 
48
The dedicated threads are created once at initialization time.
 
49
<P>
 
50
A second effect of this flag is to switch to a more concurrent
 
51
implementation of <TT>GC_malloc_many</tt>, so that free lists can be
 
52
built, and memory can be cleared, by more than one thread concurrently.
 
53
<LI>
 
54
Building the collector with -DTHREAD_LOCAL_ALLOC adds support for thread
 
55
local allocation.  It does not, by itself, cause thread local allocation
 
56
to be used.  It simply allows the use of the interface in 
 
57
<TT>gc_local_alloc.h</tt>.
 
58
<P>
 
59
Memory returned from thread-local allocators is completely interchangeable
 
60
with that returned by the standard allocators.  It may be used by other
 
61
threads.  The only difference is that, if the thread allocates enough
 
62
memory of a certain kind, it will build a thread-local free list for
 
63
objects of that kind, and allocate from that.  This greatly reduces
 
64
locking.  The thread-local free lists are refilled using 
 
65
<TT>GC_malloc_many</tt>.
 
66
<P>
 
67
An important side effect of this flag is to replace the default
 
68
spin-then-sleep lock to be replace by a spin-then-queue based implementation.
 
69
This <I>reduces performance</i> for the standard allocation functions,
 
70
though it usually improves performance when thread-local allocation is
 
71
used heavily, and thus the number of short-duration lock acquisitions
 
72
is greatly reduced.
 
73
</ul>
 
74
<P>
 
75
The easiest way to switch an application to thread-local allocation is to
 
76
<OL>
 
77
<LI> Define the macro <TT>GC_REDIRECT_TO_LOCAL</tt>,
 
78
and then include the <TT>gc.h</tt>
 
79
header in each client source file.
 
80
<LI> Invoke <TT>GC_thr_init()</tt> before any allocation.
 
81
<LI> Allocate using <TT>GC_MALLOC</tt>, <TT>GC_MALLOC_ATOMIC</tt>,
 
82
and/or <TT>GC_GCJ_MALLOC</tt>.
 
83
</ol>
 
84
<H2>The Parallel Marking Algorithm</h2>
 
85
We use an algorithm similar to
 
86
<A HREF="http://www.yl.is.s.u-tokyo.ac.jp/gc/">that developed by
 
87
Endo, Taura, and Yonezawa</a> at the University of Tokyo.
 
88
However, the data structures and implementation are different,
 
89
and represent a smaller change to the original collector source,
 
90
probably at the expense of extreme scalability.  Some of
 
91
the refinements they suggest, <I>e.g.</i> splitting large
 
92
objects, were also incorporated into out approach.
 
93
<P>
 
94
The global mark stack is transformed into a global work queue.
 
95
Unlike the usual case, it never shrinks during a mark phase.
 
96
The mark threads remove objects from the queue by copying them to a
 
97
local mark stack and changing the global descriptor to zero, indicating
 
98
that there is no more work to be done for this entry.
 
99
This removal
 
100
is done with no synchronization.  Thus it is possible for more than
 
101
one worker to remove the same entry, resulting in some work duplication.
 
102
<P>
 
103
The global work queue grows only if a marker thread decides to
 
104
return some of its local mark stack to the global one.  This
 
105
is done if the global queue appears to be running low, or if
 
106
the local stack is in danger of overflowing.  It does require
 
107
synchronization, but should be relatively rare.
 
108
<P>
 
109
The sequential marking code is reused to process local mark stacks.
 
110
Hence the amount of additional code required for parallel marking
 
111
is minimal.
 
112
<P>
 
113
It should be possible to use generational collection in the presence of the
 
114
parallel collector, by calling <TT>GC_enable_incremental()</tt>.
 
115
This does not result in fully incremental collection, since parallel mark
 
116
phases cannot currently be interrupted, and doing so may be too
 
117
expensive.
 
118
<P>
 
119
Gcj-style mark descriptors do not currently mix with the combination
 
120
of local allocation and incremental collection.  They should work correctly
 
121
with one or the other, but not both.
 
122
<P>
 
123
The number of marker threads is set on startup to the number of
 
124
available processors (or to the value of the <TT>GC_NPROCS</tt>
 
125
environment variable).  If only a single processor is detected,
 
126
parallel marking is disabled.
 
127
<P>
 
128
Note that setting GC_NPROCS to 1 also causes some lock acquisitions inside
 
129
the collector to immediately yield the processor instead of busy waiting
 
130
first.  In the case of a multiprocessor and a client with multiple
 
131
simultaneously runnable threads, this may have disastrous performance
 
132
consequences (e.g. a factor of 10 slowdown). 
 
133
<H2>Performance</h2>
 
134
We conducted some simple experiments with a version of
 
135
<A HREF="gc_bench.html">our GC benchmark</a> that was slightly modified to
 
136
run multiple concurrent client threads in the same address space.
 
137
Each client thread does the same work as the original benchmark, but they share
 
138
a heap.
 
139
This benchmark involves very little work outside of memory allocation.
 
140
This was run with GC 6.0alpha3 on a dual processor Pentium III/500 machine
 
141
under Linux 2.2.12.
 
142
<P>
 
143
Running with a thread-unsafe collector,  the benchmark ran in 9
 
144
seconds.  With the simple thread-safe collector,
 
145
built with <TT>-DLINUX_THREADS</tt>, the execution time
 
146
increased to 10.3 seconds, or 23.5 elapsed seconds with two clients.
 
147
(The times for the <TT>malloc</tt>/i<TT>free</tt> version
 
148
with glibc <TT>malloc</tt>
 
149
are 10.51 (standard library, pthreads not linked),
 
150
20.90 (one thread, pthreads linked),
 
151
and 24.55 seconds respectively. The benchmark favors a
 
152
garbage collector, since most objects are small.)
 
153
<P>
 
154
The following table gives execution times for the collector built
 
155
with parallel marking and thread-local allocation support
 
156
(<TT>-DGC_LINUX_THREADS -DPARALLEL_MARK -DTHREAD_LOCAL_ALLOC</tt>).  We tested
 
157
the client using either one or two marker threads, and running
 
158
one or two client threads.  Note that the client uses thread local
 
159
allocation exclusively.  With -DTHREAD_LOCAL_ALLOC the collector
 
160
switches to a locking strategy that is better tuned to less frequent
 
161
lock acquisition.  The standard allocation primitives thus peform
 
162
slightly worse than without -DTHREAD_LOCAL_ALLOC, and should be
 
163
avoided in time-critical code.
 
164
<P>
 
165
(The results using <TT>pthread_mutex_lock</tt>
 
166
directly for allocation locking would have been worse still, at
 
167
least for older versions of linuxthreads.
 
168
With THREAD_LOCAL_ALLOC, we first repeatedly try to acquire the
 
169
lock with pthread_mutex_try_lock(), busy_waiting between attempts.
 
170
After a fixed number of attempts, we use pthread_mutex_lock().)
 
171
<P>
 
172
These measurements do not use incremental collection, nor was prefetching
 
173
enabled in the marker.  We used the C version of the benchmark.
 
174
All measurements are in elapsed seconds on an unloaded machine.
 
175
<P>
 
176
<TABLE BORDER ALIGN="CENTER">
 
177
<TR><TH>Number of threads</th><TH>1 marker thread (secs.)</th>
 
178
<TH>2 marker threads (secs.)</th></tr>
 
179
<TR><TD>1 client</td><TD ALIGN="CENTER">10.45</td><TD ALIGN="CENTER">7.85</td>
 
180
<TR><TD>2 clients</td><TD ALIGN="CENTER">19.95</td><TD ALIGN="CENTER">12.3</td>
 
181
</table>
 
182
<PP>
 
183
The execution time for the single threaded case is slightly worse than with
 
184
simple locking.  However, even the single-threaded benchmark runs faster than
 
185
even the thread-unsafe version if a second processor is available.
 
186
The execution time for two clients with thread local allocation time is
 
187
only 1.4 times the sequential execution time for a single thread in a
 
188
thread-unsafe environment, even though it involves twice the client work.
 
189
That represents close to a
 
190
factor of 2 improvement over the 2 client case with the old collector.
 
191
The old collector clearly
 
192
still suffered from some contention overhead, in spite of the fact that the
 
193
locking scheme had been fairly well tuned.
 
194
<P>
 
195
Full linear speedup (i.e. the same execution time for 1 client on one
 
196
processor as 2 clients on 2 processors)
 
197
is probably not achievable on this kind of
 
198
hardware even with such a small number of processors,
 
199
since the memory system is
 
200
a major constraint for the garbage collector,
 
201
the processors usually share a single memory bus, and thus
 
202
the aggregate memory bandwidth does not increase in
 
203
proportion to the number of processors. 
 
204
<P>
 
205
These results are likely to be very sensitive to both hardware and OS
 
206
issues.  Preliminary experiments with an older Pentium Pro machine running
 
207
an older kernel were far less encouraging.
 
208
 
 
209
</body>
 
210
</html>