scale.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. </body>
  209. </html>