-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrustgc_paper.ltx
More file actions
2255 lines (1978 loc) · 109 KB
/
rustgc_paper.ltx
File metadata and controls
2255 lines (1978 loc) · 109 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[acmsmall]{acmart}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[ruled, vlined, linesnumbered]{algorithm2e}
\usepackage{adjustbox}
\usepackage{booktabs}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{overpic}
\usepackage{xcolor}
\usepackage{listings}
\usepackage{listings-rust}
\usepackage{microtype}
\usepackage{import}
\usepackage{multicol}
\usepackage{multirow}
\usepackage{pdflscape}
\usepackage{siunitx}
\usepackage{subcaption}
\usepackage{tikz}
\usepackage{xspace}
\usepackage{ifthen}
\usepackage{geometry}
% Must be loaded last
\usepackage[capitalise]{cleveref}
\def\mathdefault#1{#1}
\lstset{
language=Rust,
basicstyle=\selectfont\tt\color{black},
keywordstyle={}
}
% from https://tex.stackexchange.com/questions/264361/skipping-line-numbers-in-lstlisting#264373
\let\origthelstnumber\thelstnumber
\makeatletter
\newcommand*\Suppressnumber{%
\lst@AddToHook{OnNewLine}{%
\let\thelstnumber\relax%
\advance\c@lstnumber-\@ne\relax%
}%
}
% Algorithm macros
\SetAlFnt{\ttfamily\footnotesize}
\SetKwIF{If}{ElseIf}{Else}{\texttt{\textbf{if}}}{\texttt{\textbf{then}}}{\texttt{\textbf{else if}}}{\texttt{\textbf{else}}}{\texttt{\textbf{endif}}}
\SetKw{ForEach}{\texttt{\textbf{for each}}}
\SetKwProg{Function}{\texttt{\textbf{function}}}{:}{}
\SetKw{AND}{\texttt{\textbf{and}}}
\SetKw{OR}{\texttt{\textbf{or}}}
\SetKw{NOT}{\texttt{\textbf{not}}}
\SetKw{Continue}{\texttt{\textbf{continue}}}
\SetKw{Return}{\texttt{\textbf{return}}}
\SetKwBlock{DoBlock}{\texttt{\textbf{do}}}{}
\newcommand\boehm{\textsc{BDWGC}\xspace}
\newcommand\rustbacon{\textsc{Bacon-Rajan-CC}\xspace}
\newcommand\rustc{\lstinline{rustc}\xspace}
\newcommand\bronze{\textsc{Bronze}\xspace}
\newcommand\ourgc{\textsc{Alloy}\xspace}
\newcommand\rustgcproj{\textsc{Rust-GC}\xspace}
\newcommand\gcvsbaseline{\textsc{RC [jemalloc]}\xspace}
\newcommand\gcvsrc{\textsc{RC [gcmalloc]}\xspace}
\newcommand\shifgrethor{\textsc{Shifgrethor}\xspace}
\newcommand\gcarena{\textsc{gc-arena}\xspace}
\newcommand\bor{borrow-or-finalize rule\xspace}
\newcommand\somrsrc{\textsc{rc$_{\textrm{jemalloc}}$}\xspace}
\newcommand\somrsrcbdwgc{\textsc{rc$_{\textrm{bdwgcalloc}}$}\xspace}
\newcommand\somrsgc{\textsc{gc}\xspace}
\newcommand\somrs{\textsc{som-rs}\xspace}
\newcommand\yksomnaive{\textsc{YKSOM$_{\textrm{Naive}}$}\xspace}
\newcommand\yksomnobarriers{\textsc{BarriersNone}\xspace}
\newcommand\yksomallbarriers{\textsc{BarriersAll}\xspace}
\newcommand\yksomoptbarriers{\textsc{BarriersOpt}\xspace}
\newcommand\perf{\textsc{perf}\xspace}
\newcommand\mem{\textsc{mem}\xspace}
\newcommand\bopt{\textsc{BarriersNone}\xspace}
\newcommand\bnone{\textsc{BarriersAll}\xspace}
\newcommand\bnaive{\textsc{BarriersOpt}\xspace}
\newcommand\felide{\textsc{Elision}\xspace}
\newcommand\fnaive{\textsc{Naive}\xspace}
\newcommand\Egcrc{E$_\textrm{GCvs}$\xspace}
\newcommand\Eelision{E$_\textrm{Elision}$\xspace}
\newcommand\Epremopt{E$_\textrm{PremOpt}$\xspace}
\newcommand\rustcversion{1.79.0\xspace}
\lstdefinestyle{rustblock}{
language=Rust,
numbersep=5pt,
xleftmargin=10pt,
numberstyle=\fontsize{5}{8}\selectfont\tt\color{gray},
basicstyle=\fontsize{8}{9}\selectfont\tt\color{black},
keywordstyle=\bfseries, % reserved keywords
keywordstyle=[2]\color[rgb]{0.75, 0, 0},% traits
keywordstyle=[3]\color[rgb]{0, 0.5, 0},% primitive types
keywordstyle=[4]\color[rgb]{0, 0.5, 0},% type and value constructors
keywordstyle=[5]\color[rgb]{0, 0, 0.75},% macros
captionpos=b,
escapeinside={{<!}{!>}},
numbers=left,
tabsize=2,
breakatwhitespace=false,
breaklines=false,
showstringspaces=false,
showspaces=false,
columns=fullflexible,
language=Rust
}
\lstdefinelanguage{FsaError}{%
sensitive%
, morecomment=[l]{//}%
, morecomment=[s]{/*}{*/}%
, morestring=[b]{"}%
, alsodigit={}%
, alsoother={}%
, alsoletter={!}%
, morekeywords={let, fn, self, mut} % control flow keywords
}%
\definecolor{fsared}{rgb}{0.75,0,0}
\definecolor{fsaorange}{rgb}{0.85,0.40,0}
\lstdefinestyle{fsaerror}{
language=FsaError,
numbersep=5pt,
numberstyle=\fontsize{5}{8}\selectfont\tt\color{gray},
basicstyle=\fontsize{8}{9}\selectfont\tt\color{black},
keywordstyle=\bfseries, % reserved keywords
moredelim=**[is][\color{fsared}]{@}{@},
moredelim=**[is][\color{fsaorange}]{~}{~},
captionpos=b,
numbers=left,
commentstyle=\color{blue!66}\it,
tabsize=2,
breakatwhitespace=false,
breaklines=false,
showstringspaces=false,
showspaces=false,
columns=fullflexible,
keepspaces=true,
}
%%% The following is specific to OOPSLA2 '25 and the paper
%%% 'Garbage Collection for Rust: The Finalizer Frontier'
%%% by Jacob Hughes and Laurence Tratt.
%%%
\setcopyright{cc}
\setcctype{by}
\acmDOI{10.1145/3763179}
\acmYear{2025}
\acmJournal{PACMPL}
\acmVolume{9}
\acmNumber{OOPSLA2}
\acmArticle{401}
\acmMonth{10}
\received{2025-03-26}
\received[accepted]{2025-08-12}
\begin{document}
\author{Jacob Hughes}
\orcid{0009-0002-5321-0645}
\affiliation{%
\institution{King's College London}
\city{London}
\country{United Kingdom}
}
\email{jh@jakehughes.uk}
\author{Laurence Tratt}
\orcid{0000-0002-5258-3805}
\affiliation{%
\institution{King's College London}
\city{London}
\country{United Kingdom}
}
\email{laurie@tratt.net}
\title{Garbage Collection for Rust: The Finalizer Frontier}
\include{experiment_stats}
\include{macros}
\begin{abstract}
\noindent Rust is a non-Garbage Collected (GCed) language, but the lack of GC
makes expressing data-structures that require shared ownership awkward,
inefficient, or both. In this paper we explore a new design for, and implementation of, GC in
Rust, called \ourgc. Unlike previous
approaches to GC in Rust, \ourgc allows existing Rust destructors to be automatically
used as GC finalizers: this makes \ourgc integrate better with existing Rust code
than previous solutions but introduces surprising soundness and performance
problems. \ourgc provides novel solutions for the core problems:
\emph{finalizer safety analysis} rejects unsound destructors from
automatically being reused as finalizers; \emph{finalizer elision} optimises
away unnecessary finalizers; and \emph{premature finalizer prevention}
ensures that finalizers are only run when it is provably safe to do so.
\end{abstract}
\begin{CCSXML}
<ccs2012>
<concept>
<concept_id>10011007.10011006.10011041</concept_id>
<concept_desc>Software and its engineering~Compilers</concept_desc>
<concept_significance>500</concept_significance>
</concept>
<concept>
<concept_id>10011007.10010940.10010941.10010949.10010950.10010954</concept_id>
<concept_desc>Software and its engineering~Garbage collection</concept_desc>
<concept_significance>500</concept_significance>
</concept>
</ccs2012>
\end{CCSXML}
\ccsdesc[500]{Software and its engineering~Compilers}
\ccsdesc[500]{Software and its engineering~Garbage collection}
\keywords{Compilers, Garbage collection, Rust}
\maketitle
\section{Introduction}
\begin{figure}[t]
\lstinputlisting[
style=rustblock,
firstline=6,
caption={An \ourgc example, showing \lstinline{Gc<T>}
and destructors as finalizers. We create a type \lstinline{GcNode} which
models a graph: it stores an 8 bit integer value and a possibly-null
reference (via Rust's standard \lstinline{Option} type) to a neighbouring node
(line 1). We add a normal Rust destructor which \ourgc is able to use as a
finalizer when \lstinline{GcNode} is used inside \lstinline{Gc<T>} (line 2).
Inside \lstinline{main} we create the first GCed node in the graph (line 5).
We use Rust's normal \lstinline{RefCell} type to allow the node to be mutated
(using the \lstinline{RefCell::borrow\_mut} method which dynamically checks
for mutation that would undermine Rust's static rules)
and a cycle created directly back to itself (line 6). We then create a second cyclic graph (lines 7
and 8), immediately assigning it to the \lstinline{gc1} variable (line 9):
this copies, rather than moves, the \lstinline{Gc<T>}.
This causes the first cyclic graph \lstinline{GcNode\{value: 1, ..\}}
to no longer be reachable, so after forcing a collection (line 10) that node
can be collected. Its finalizer is then scheduled to be run, causing
\lstinline{drop 1} to be printed out at a later point; when it has completed the GC
heap memory can be reclaimed. The print statement outputs \lstinline{2 2} (line
11).},
label={fig:first_example}
]{listings/first_example.rs}
\end{figure}
Amongst the ways one can classify programming languages are whether they
are Garbage Collected (GCed) or not: GCed languages enable implicit memory management;
non-GCed languages require explicit memory management (e.g~\lstinline{C}'s \lstinline{malloc} /
\lstinline{free} functions). Rust's use of affine types~\citep[p.~5]{pierce04advanced}
and ownership does not fit within this classification: it is not GCed but it has implicit scope-based memory management.
Most portions of Rust programs are as
succinct as a GCed equivalent, but ownership is too inflexible to express
\emph{shared ownership} for data-structures that require multiple owners
(e.g.~doubly linked lists).
Workarounds such as reference counting impose an extra burden on the programmer,
make mistakes more likely, and often come with a performance penalty.
In an attempt to avoid such problems, there are now a number of GCs for Rust
(e.g.~\cite{manish15rustgc, coblenz21bronze, gcarena, boa, shifgrethor}). Most
introduce a user-visible type \lstinline{Gc<T>} which takes a value $v$
of type \lstinline{T} and moves $v$ to the `GC heap'. The \lstinline{Gc<T>}
value itself is a wrapper around a pointer to $v$ on the GC heap.
\lstinline{Gc<T>} can be \emph{cloned} (i.e.~duplicated) and
\emph{dereferenced} to a value of type \lstinline{&T} (i.e.~a type-safe pointer) at will by the user. When no
\lstinline{Gc<T>} wrappers pointing to $v$
can be found, indirectly or directly, from the
program's \emph{roots} (e.g.~variables on the stack),
then the GC heap memory for $v$ can be reclaimed.
It has proven hard to find a satisfying design and implementation for a GC for
Rust, as perhaps suggested by the number of attempts to do so.
We identify two fundamental challenges
for GC for Rust: how to give \lstinline{Gc<T>} an idiomatic and complete
API; and how to make \emph{finalizers} (i.e.~the code that is run just before a
value is collected by the GC) safe, performant, and ergonomic.
In this paper we introduce \ourgc, a new GC for Rust: an example of its use is
shown in \cref{fig:first_example}. \ourgc uses \emph{conservative} garbage
collection (i.e.~treating each reachable machine word as a potential pointer),
which naturally solves the API challenge. However, the finalization challenge
is much more involved: the causes of this challenge, and our solutions to
it, occupy the bulk of this paper.
Normal Rust code uses \emph{destructors} (i.e.~code which is run just before a
value is reclaimed by Rust's implicit memory management) extensively. Although
finalizers and destructors may seem to be synonyms, existing GCs for Rust
cannot reuse destructors as finalizers:
the latter must be manually implemented for each type that needs it.
Unfortunately, even this is trickier than it appears:
it is not possible to implement a finalizer for
\lstinline{Gc<T>} if \lstinline{T} is an external library; some parts of
destructors are automatically created by the Rust compiler, but
hand-written finalizers must duplicate those parts manually; and users
can accidentally cause a type's finalizer to be run more than once. In
short, finalization in existing GCs for Rust is unpalatable.
GCs for Rust are not alone in requiring manually written finalizers.
In a close cousin to our work,
a GC proposal for C++, the reuse of destructors as finalizers was ruled out due to
seemingly insoluble problems~\cite[p.~32]{boehm09garbage}, which we divide
into four categories:
(1) some safe destructors are not safe finalizers;
(2) finalizers can be run prematurely;
(3) running finalizers on the same thread as a paused mutator can cause race conditions and deadlocks;
(4) and finalizers are prohibitively slower than destructors.
All are, at least to some degree, classical GC problems; all are exacerbated
in some way by Rust; and none, with the partial exception of \#2, has
existing solutions.
We show that it is possible to reuse most
Rust destructors as finalizers in a satisfying way. We introduce novel solutions to the
long-standing problems this implies by making use of some of Rust's unusual
static guarantees. We thus gain a better GC for
Rust \emph{and} solutions to open GC problems. Our solutions, in order, are:
(1) \emph{finalizer safety analysis}
extends Rust's static analyses to reject programs whose destructors are not
provably safe to be used as finalizers;
(2) \emph{premature finalizer prevention} automatically inserts fences to prevent
the GC from being `tricked' into collecting values before they are dead;
(3) we run finalizers on a separate thread; and
(4) and \emph{finalizer elision} statically optimises away finalizers if the
underlying destructor duplicates the GC's work.
\ourgc as an implementation
is necessarily tied to Rust, though most of the novel techniques in this paper rely on
general properties of affine types and ownership. While we do not wish
to claim generality without evidence, it seems likely that
many of the techniques in this paper will generalise to other
ownership-based languages, as and when such emerge.
Although \ourgc is not production ready, its performance is already
reasonable: when we control for the (admittedly somewhat slow) conservative GC (\boehm)
\ourgc currently uses, the performance of \ourgc varies from
\gcvsgcperfbestratio to, in the worst case, \gcvsgcperfworstratio that of
reference counting. \ourgc is also sufficiently polished (e.g.~good quality error messages)
in other ways for it to: show a plausible path forwards for those who may
wish to follow it; and to allow others to evaluate whether GC for Rust is a good
idea or not.
This paper is divided into four main parts: GC and Rust background (\cref{sec:background});
\ourgc's basic design (\cref{sec:alloy_design}); destructor and finalizer challenges
and solutions (\cref{sec:destructor challenges,sec:elision,sec:premature_finalize_prevention,sec:fsa}); and evaluation
(\cref{sec:evaluation}). The first three parts have the challenge that our work
straddles two areas that can seem mutually exclusive: GC and Rust. We have
tried to provide sufficient material for readers expert in one of these
areas to gain adequate familiarity with the other, without boring either, but we encourage
readers to skip material they are already comfortable with.
\section{Background}
\label{sec:background}
\subsection{The Challenges of Shared Ownership in Rust}
\begin{figure}[t]
\lstinputlisting[
style=rustblock,
firstline=5,
caption={
A version of~\cref{fig:first_example} using Rust's standard reference
counting type \lstinline{Rc<T>}. To avoid memory leaks we use \emph{weak}
references between nodes (line 1). We again create two cyclic graphs (lines
5--8) using \lstinline{Rc::downgrade} to create weak references (lines 6 and
8). Since \lstinline{Rc<T>} is not copyable, we must use a manual
\lstinline{clone} call to have both the \lstinline{rc1} and \lstinline{rc2}
variables point to the same cyclic graph (line 9). Accessing a neighbour
node becomes a delicate dance requiring upgrading the weak reference (line 11).
The need to downgrade \lstinline{Rc<T>} to \lstinline{Weak<T>} and upgrade
(which may fail, hence the \lstinline{unwrap}) back to \lstinline{Rc<T>}
creates significant extra complexity relative to~\cref{fig:first_example}: compare
line 11 in \cref{fig:first_example} to lines 10-11 above.
},
label={fig:rc_example}
]{listings/rc_example.rs}
\end{figure}
Rust uses affine types and \emph{ownership}
to statically guarantee that: a value has a
single owner (e.g.~a variable); an owner can \emph{move} (i.e.~permanently
transfer the ownership of) a value to another owner; and
when a value's owner goes out of scope, the value's destructor
is run and its backing memory reclaimed. An owner can pass \emph{references} to a value
to other code, subject to the following static restrictions: there can be
multiple immutable references (`\lstinline{&}') to a value or a single
mutable reference (`\lstinline{&mut}'); and references cannot outlast the owner.
These rules allow many Rust programs to be as succinct as their equivalents
in GCed languages. This suggests that the search for a good GC for Rust may be
intellectually stimulating but of little practical value.
However, there are many programs which need to express data structures
which do not fit into the restrictions of affine types and
ownership. These are often described as `cyclic data-structures', but
in this paper we use the more abstract term `shared ownership', which includes,
but is not limited to, cyclic data-structures.
A common way of expressing shared ownership is to use
the reference counting type \lstinline{Rc<T>} from Rust's
standard library. For many data-structures, this is a reasonable
solution, but some forms of shared ownership require
juggling strong and weak counts. This complicates programs
(see~\cref{fig:rc_example}) and can cause problems when
values live for shorter or longer than intended.
A different solution is to store values in a vector and use
integer indices into that vector. Such indices are morally closer to
machine pointers than normal Rust references: the indices can become
stale, dangle, or may never have been valid in the first place. The programmer
must also manually deal with issues such as detecting unused values,
compaction, and so on. In other words, the programmer
ends up writing a partial GC themselves. A variant of this idea are
\emph{arenas}, which gradually accumulate multiple values but free all of them in one go: values
can no longer be reclaimed too early, though arenas tend to unnecessarily
increase the lifetime of values.
A type-based approach is
\lstinline{GhostCell}~\cite{yanovski21ghostcell}, which uses \emph{branding} to
statically ensure that at any given point only one part of a program can access a
shared ownership data-structure. This necessarily excludes common use cases
where multiple owners (e.g.~in different threads) need to simultaneously access
disjoint parts of a data-structure.
Although it is easily overlooked, some workarounds (e.g.~\lstinline{Rc<T>})
rely on using \emph{unsafe} Rust (i.e.~parts of the language, often involving
pointers, that are not fully statically checked by the compiler). Pragmatically,
we assume that widely used code, even if technically unsafe, has been pored
over sufficiently that it is trustworthy in practise. However,
`new' solutions that a programmer implements using
unsafe Rust are unlikely to immediately reach the same level of trustworthiness.
While we do not believe that every Rust program would be improved by GC, the
variety of workarounds already present in Rust code, and the difficultly
of creating new ones, suggests that there is a subset that would benefit from GC.
\subsection{GC Terminology}
GC is a venerable field and has accumulated terminology that can seem
unfamiliar or unintuitive. We mostly use the same terminology
as~\citet{jones23garbage}, the major parts of which we define here.
A program which uses GC is split between the \emph{mutator} (the user's program) and
the \emph{collector} (the GC itself). At any given point in time, a thread is either
running as a mutator or a collector. In our context, all threads
run as a collector at least sometimes (for reasons that will become apparent
later, some threads always run as a collector).
Tracing and reclamation is performed during a \emph{collection} phase. Our
collections always \emph{stop-the-world}, where all threads running
mutator code are paused while collection occurs.
A \emph{tracing} GC is one that scans memory looking
for reachable values from a program's roots: values , including cycles of
values, that are not reachable from the roots can then be \emph{reclaimed}. In
contrast, a pure reference counting GC does not scan memory, and thus cannot
free values that form a cycle. Increasingly, GC implementations make use of
multiple techniques (see~\cite{bacon04unified}) but, for simplicity's sake,
we assume that implementations wholly use one
technique or another except otherwise stated. For brevity, we use `GC' as a short-hand for `tracing
GC'; when we deal with other kinds of GC (e.g.~reference counting), we
explicitly name them.
We refer to memory which is allocated via \lstinline{Gc<T>} as being on
the \emph{GC heap}. We use the term `GC value' to refer both to the pointer wrapped in a
\lstinline{Gc<T>} and the underlying value on the GC heap, even though multiple
pointers / wrappers can refer to a single value on the GC heap, unless doing so
would lead to ambiguity.
We use `\ourgc' to refer to the combination of: our extension to the Rust
language; our modifications to the \lstinline{rustc} compiler; and our
integration of the Boehm-Demers-Weiser GC (\boehm) into the runtime of programs
compiled with our modified \lstinline{rustc}.
\section{\ourgc: Design and Implementation}
\label{sec:alloy_design}
In this section we outline \ourgc's basic design and implementation choices --
the rest of the paper then goes into detail on the more advanced aspects.
\subsection{Basic Design}
\label{sec:basic design}
\ourgc provides a \lstinline{Gc<T>} type that exposes an API modelled on the
\lstinline{Rc<T>} type from Rust's standard library, because
\lstinline{Rc<T>}: is conceptually
similar to \lstinline{Gc<T>}; widely used in Rust code, and its API
familiar; and that API reflects long-term experience about what Rust programmers
need.
When a user calls \lstinline{Gc::new(v)}, the value \lstinline{v} is
moved to the GC heap: the \lstinline{Gc<T>} value returned to the user is a
simple wrapper around a pointer to \lstinline{v}'s new address. The same underlying GCed value
may thus have multiple, partly or wholly overlapping, references active at any point.
To avoid undermining
Rust's ownership system, dereferencing a \lstinline{Gc<T>}
produces an immutable (i.e.~`\lstinline{&}') reference to the underlying value.
If the user wishes to mutate the underlying value, they must use other Rust
types that enable \emph{interior mutability} (e.g.~\lstinline{RefCell<T>} or
\lstinline{Mutex<T>}).
One feature that \ourgc explicitly supports is the ability in Rust to
cast references to raw pointers and back again. This can occur in two main
ways. \lstinline{Gc<T>} can be dereferenced to \lstinline{&T} which
can then, as with any other reference, be converted to \texttt{*const T}
(i.e.~a C-esque pointer to T). \lstinline{Gc<T>} also supports the common Rust
functions (\lstinline{into_raw} and \lstinline{from_raw}) which wrap
the value-to-pointer conversion in a slightly higher-level API. The ability to
convert references to raw pointers is used in many places (e.g.~Rust's standard
C Foreign Function Interface (FFI)).
We believe that a viable GC for Rust must allow the same conversions,
but doing so has a profound impact because Rust allows raw pointers to be
converted to the integer type \lstinline{usize} and back\footnote{Although
it is outside the scope of this paper, it would
be preferable for Rust to have different types for `data width' and
`address'. Modern C, for example, captures this difference with the \lstinline{size_t} and
\lstinline{uintptr_t} types respectively. Rust now has a provenance lint to
nudge users in this general direction, but the \lstinline{as}
keyword still allows arbitrary conversions.}.
\label{conservative_gc}
Having acknowledged that pointers can be `disguised' as integers, it is then
inevitable that \ourgc must be a conservative GC: if a machine word's integer
value, when considered as a pointer, falls within a GCed block of memory,
then that block itself is considered reachable (and is transitively scanned).
Since a conservative GC cannot know if a word is really a pointer, or is a random sequence of
bits that happens to be the same as a valid pointer, this over-approximates the
\emph{live set} (i.e.~the blocks that the GC will not reclaim). Typically
the false detection rate is very low (see e.g.~a Java study which measures
it at under 0.01\% of the live set~\cite{shahriyar14fast}).
Conservative GC occupies a grey zone in programming language semantics: in most
languages, and most compiler's internal semantics, conservative GC is, formally
speaking, unsound; and furthermore some languages (including Rust) allow
arbitrary `bit fiddling' on pointers, temporarily
obscuring the address they are referring to. Despite this, conservative GC is widely used,
including in the two most widespread web browsers: Chrome uses it in its Blink
rendering engine~\cite{ager13oilpan} and Safari uses it in its JavaScript VM
JavaScriptCore~\cite{pizlo17riptide}. Even in 2025, we lack good alternatives
to conservative GC: there is no cross-platform API for precise GC; and while
some compilers such as LLVM provide some support for GC
features~\cite{llvm14statepoints}, we have found them incomplete and buggy.
Despite the potential soundness worries, conservative GC thus remains a widely
used technique.
\label{gc_is_copyable}
Conservative GC enables \ourgc to make a useful ergonomic improvement over
most other GCs for Rust whose \lstinline{Gc<T>} is only \emph{cloneable}. Such types can be duplicated, but doing
so requires executing arbitrary user code. To make the possible run-time cost of this clear, Rust has
no direct syntax for cloning: users must explicitly call \lstinline{Rc::clone(&v)}
to duplicate a value \lstinline{v}. In contrast, since \ourgc's \lstinline{Gc<T>} is just a wrapper around a pointer it
is not just cloneable but also \emph{copyable}: duplication only requires copying
bytes (i.e.~no arbitrary user code need be executed). Copying is implied by assignment
(i.e. \lstinline{w = v}),
reducing the need for explicit cloning\footnote{The lengthier
syntax \lstinline{y = Gc::clone(&v)} is available, since every copyable type is
also cloneable.}. This is not just a syntactic convenience but also reflects an underlying
semantic difference: duplicating a \lstinline{Gc<T>} in \ourgc is is a cheaper and simpler operation
than most other GCs for Rust which which tend to rely, at least in part, on reference counting.
There is one notable limitation of \lstinline{Gc<T>}'s API relative to
\lstinline{Rc<T>}. The latter, by definition, knows how many references
there are to the underlying data, allowing the value stored inside it
to be mutably borrowed at run-time if there is only a single reference to it
(via \lstinline{get_mut} and \lstinline{make_mut}).
In contrast, \lstinline{Gc<T>} cannot know how many references
there are to the underlying data. As we shall see in~\cref{sec:evaluation}, some Rust programs
are built around the performance advantages of this API (e.g.~turning
`copy on write' into just `write' in some important cases).
\subsection{Basic Implementation}
The most visible aspect of \ourgc is its fork, and extension of, the standard
Rust compiler \rustc. We forked \rustc~\rustcversion, adding
or changing approximately 5,500 Lines of Code (LoC) in the core compiler,
and adding approximately 2,250 LoC of tests.
\ourgc uses \boehm~\cite{boehm88garbage} as the underlying conservative GC, because it is the
most widely ported conservative GC we know of. We use \boehm's \lstinline{GC_set_finalize_on_demand(1)} API,
which causes finalizers to be run on their own thread.
We had to make some minor changes to \boehm to suit our situation.
First, we disabled \boehm's parallel collector
because it worsens \ourgc's performance. It is unclear to us why this happens:
we observe significant lock contention within \boehm during GC
collections, but have not correlated this with a cause.
Second, \boehm cannot scan pointers stored in thread locals
because these are platform dependent. Fortunately, \rustc uses LLVM's
thread local storage implementation, which stores such pointers in the
\lstinline{PT_TLS} segment of the ELF binary: we modified \boehm to scan
this ELF segment during each collection. Third,
\boehm dynamically intercepts thread creation calls so that it can
can scan their stacks, but (for bootstrapping
reasons) is unable to do so in our context: we explicitly changed \ourgc
to register new threads with \boehm.
\section{Destructors and Finalizers}
\label{sec:destructor challenges}
In many GCed languages, `destructor' and `finalizer' are used as synonyms, as
both terms refer to code run when a value's lifetime has ended. In existing GCs
for Rust, these two terms refer to completely different hierarchies of code (i.e.~destructors
and finalizers are fundamentally different). In \ourgc, in contrast, a reasonable first
approximation is that finalizers are a strict subset of destructors. In this section we pick apart
these differences, before describing the challenges of using destructors as
finalizers.
When a value in Rust is \emph{dropped} (i.e.~at the point its owner goes out of lexical
scope) its destructor is automatically run. Rust's destructors enable a style
of programming that originated in C++ called RAII (Resource Acquisition Is
Initialization)~\cite[Section~14.4]{stroustrup97c++}: when a value is dropped,
the underlying resources it possesses (e.g.~file handles or heap memory) are
released. Destructors are used frequently in Rust code (to give a rough idea: approximately 15\%
of source-level types in our benchmark suite have destructors).
Rust destructors are formed of two
parts, run in the following order: a user-defined \emph{drop method}; and
automatically inserted \emph{drop glue}. Drop methods are optional and users
can provide one for a type by implementing the \lstinline{Drop} trait's \lstinline{drop}
method. Drop glue recursively calls destructors of contained types (e.g.~fields
in a \texttt{struct}). Although it is common usage to conflate `destructor' in
Rust with drop methods, drop glue is an integral part of a Rust destructor:
we therefore use `destructor' as the umbrella term for both drop methods and drop glue.
When considering finalizers for a GC for Rust, there are several layers of
design choices. We will shortly see that finalizers cause a number of
challenges (\cref{sec:general_challenges}) and one choice would be to forbid
finalizers entirely. However, this would mean that one could not sensibly embed types
that have destructors in a \lstinline{Gc<T>}. While
Rust does not always call destructors, the situations where this occurs
are best considered `exceptional': not calling destructors from
\lstinline{Gc<T>} would completely undermine reasonable programmer
expectations. Because of this, \ourgc, and indeed virtually all GCs for Rust,
support finalizers in some form.
However, existing GCs force distinct notions of destructors and finalizers onto the programmer.
Where the former have the \lstinline{Drop} trait, the latter typically have
a \lstinline{Finalize} trait. If a user type needs to be finalized then
the user must provide an implementation of the \lstinline{Finalize} trait.
However, doing so introduces a number of problems: (1) external libraries are
unlikely to provide finalizers, so they must be manually implemented
by each consumer; (2) Rust's \emph{orphan
rule}~\cite[Section~6.12]{rustlangref} prevents one implementing traits for
types defined in external libraries (i.e.~unless a library's types were
designed to support \lstinline{Gc<T>}, those types cannot be directly GCed);
(3) one cannot automatically replicate drop glue for finalizers; and (4) one
cannot replicate \rustc's refusal to allow calls to the equivalent of
\lstinline{Drop::drop}.
Programmers can work around problems \#1 and \#2 in various ways. For example,
they can wrap external library types in \emph{newtypes} (zero-cost wrappers)
and implement finalizers on those instead~\cite[Section~19.3]{klabnik18rust}.
Doing so is tedious but not conceptually difficult.
Problem \#3 has partial solutions: for example, ~\cite{manish15rustgc} uses the
\lstinline{Trace} macro to generate \emph{finalizer glue} (the finalizer equivalent of drop glue) for
\texttt{struct} fields. This runs into an unsolvable variant of problem \#2:
types in external libraries will not implement this trait and cannot be
recursively scanned for finalizer glue.
Problem \#4 is impossible to solve in Rust as-is. One cannot define a function
that can never be called --- what use would such a function have? A possible
partial solution might seem to be for the \lstinline{finalize} method take ownership of the value,
but \lstinline{Drop::drop} does not do so because that would not allow drop
glue to be run afterwards.
\subsection{General Challenges When Using Destructors as Finalizers}
\label{sec:general_challenges}
We have stated as our aim that \ourgc should use destructors as finalizers.
Above we explained some Rust-specific challenges --- but there are several
non-Rust-specific challenges too! Fundamentally, finalizers and destructors
have different, and sometimes incompatible, properties. The best
guide to these differences, and the resulting problems, is~\citet{boehm03destructors},
supplemented by later work on support
for GC in the C++ specification~\cite{boehm09garbage}\footnote{These features
were added to the C+11 specification but removed in C++23.}.
An obvious difference between destructors and finalizers is when both
are run. While C++ and Rust define
precisely when a destructor will be run\footnote{Mostly. Rust's `temporary
lifetime extension' delays destruction, but for how long is currently
unspecified.}, finalizers run at an unspecified point in time. This typically
happens at some point after the equivalent destructor would run, though
a program may exit before any given finalizer is run\footnote{A program could pause after `exit' while all queued finalizers are run. We are
not aware of a system which does this.}. There are, however, two situations which
invert this.
First, if a thread exits due to an error, and the program is either not compiled with
unwinding, or the thread has crossed a non-unwinding ABI boundary, then
destructors might not be run at all, where a GC will naturally run the
equivalent finalizers: we do not dwell on this, as both behaviours
are reasonable in their different contexts. Second, and more surprisingly, it is possible for finalizers in
non-error situations to run \emph{prematurely}, that is before the equivalent
destructor~\cite[section~3.4]{boehm03destructors}.
A less obvious difference relates to where destructors and finalizers are run.
Destructors run in the same thread as the last owner of a value.
However, running finalizers in the same thread as the last owner of the value
can lead to race conditions~\cite{niko13destructors} and
deadlocks~\cite[section~3.3]{boehm03destructors} if a finalizer tries to access
a resource that the mutator expects to have exclusive access too.
When such problems affect destructors in normal Rust code, it is the clear result of programmer error, since they should
have taken into account the predictable execution point of destructors. However, since
finalizers do not have a predictable execution point, there is no way
to safely access shared resources if they are run on the same thread.
The only way to avoid this is to run
finalizers on a non-mutator thread --- but not all Rust types / destructors
are safe to run on another thread.
There are several additional differences such as: finalizers
can reference other GCed values that are partly, or wholly, `finalized' and may
have had their backing memory reused; and finalizers can \emph{resurrect} values by
copying the reference passed to the finalizer and storing it somewhere.
Over time, finalizers have thus come to be viewed with increasing suspicion. Java,
for example, has deprecated, and intends eventually removing, per-type
finalizers: instead it has introduced deliberately less flexible per-object `cleaners', whose API
prevents problems such as object resurrection and per-class finalization~\cite{goetz21deprecated}.
\subsection{The Challenge of Finalizers for \ourgc}
At this point we hope to have convinced the reader that: a
viable GC for Rust needs to be able to use existing destructors as finalizers
whenever possible; but that finalizers, even in existing GCs, cause
various problems.
It is our belief that some problems with finalizers are fundamental. For
example, finalizers inevitably introduce latency between the last
use of a value and its finalization.
Some problems with finalizers are best considered the accidental artefacts of
older designs. Java's cleaners, for example, can be thought of as a more
restrictive version of finalizers that allow most common use-cases but forbid
by design many dangerous use cases. For example, per-class/struct finalization
can easily be replaced by per-object/value finalization; and object
resurrection can be prevented if object access requires a level of indirection.
\ourgc benefits from our better shared understanding of such problems and the
potential solutions: it trivially addresses per-object/value finalization
(\lstinline{Gc::new_unfinalizable} function turns finalization off for specific
values) and, as we shall see later, via only slightly more involved means,
object resurrection.
However, that leaves many problems that are potentially in the middle: they are
not obviously fundamental, but there are not obvious fixes for them either. In
our context, where we wish to use destructors as finalizers, four problems have
hitherto been thought insoluble~\cite[p.~32]{boehm09garbage}:
(1) finalizers are prohibitively slower than destructors;
(2) finalizers can run prematurely;
(3) running finalizers on the same thread as a paused mutator can cause race conditions and deadlocks;
(4) some safe destructors are not safe finalizers.
Fortunately for us, Rust's unusual static guarantees, suitably expanded by
\ourgc, allow us to address each problem in novel, satisfying, ways. In the following
section, we tackle these problems in the order above, noting that we tackle problems
\#1 and \#2 separately, and \#3 and \#4 together.
\section{Finalizer Elision}
\label{sec:elision}
As we shall see in \cref{sec:evaluation}, there is a correlation between the
number of finalizers that are run and overhead from GC (with a worst case,
albeit a definite outlier, in
our experiment of \elisionnaiveworstratio slowdown). In this section
we show how to reduce the number of finalizers that are run, which helps
reduce this overhead.
A variety of factors contribute to the finalizer performance overhead, including:
a queue of finalizers
must be maintained, whereas destructors can be run immediately; finalizers run
some time after the last access of a value, making cache misses more likely; and
finalizers can cause values (including values they own) to live for longer
(e.g.~leading to increased memory usage and marking overhead).
Most of these factors are inherent to any GC and
our experience of using and working on \boehm -- a mature, widely used GC -- does
not suggest that it is missing optimisations which would overcome all of this overhead.
Instead, whenever possible, \ourgc \emph{elides} finalizers so that they do not need to be run at all.
We are able to do this because: (1) \boehm is responsible for all allocations and
will, if necessary GC allocations even if they are not directly wrapped in a \lstinline{Gc<T>}; and (2) many Rust destructors
only free memory which \boehm would, albeit with some latency, do anyway.
Consider the standard Rust type \lstinline{Box<T>} which heap allocates space for a value;
when a \lstinline{Box<T>} value is dropped, the heap allocation will be freed.
We can then make two observations. First,
\lstinline{Box<T>}'s drop method solely consists of a \lstinline{deallocate}
call. Second, while we informally say
that \lstinline{Box<T>} allocates on the `heap' and \lstinline{Gc<T>} allocates
on the `GC heap', all allocations in \ourgc are made through \boehm and stored in the same
heap.
When used as a finalizer,
\lstinline{Box<T>}'s drop method is thus unneeded, as the underlying memory will
be freed by \boehm anyway.
This means that there is no need to run a finalizer for a type such as
\lstinline{Gc<Box<u8>>} at all, and the finalizer can be statically elided. However,
we cannot elide a finalizer for a type such as
\lstinline{Gc<Box<Rc<u8>>} because
\lstinline{Rc<T>}'s drop method must be run for the reference count to be decremented.
As this shows, we must consider the complete destructor, and not just the top-level
drop method, when deciding whether a corresponding finalizer can be elided.
\begin{algorithm}[t]
\small
\caption{Finalizer Elision}
\label{alg:elision}
\SetAlgoNoLine
\Function{\texttt{NeedsFinalizer(T)}}{
\uIf{\texttt{Impls(T, Drop) \AND \NOT Impls(T, DropMethodFinalizerElidable)}}{
\Return{\texttt{true}}\;
}
\SetAlgoVlined
\ForEach{field $\in$ T} {
\DoBlock{
\SetAlgoNoLine
\uIf{\texttt{NeedsFinalizer(field)}}{
\Return{\texttt{true}}\;
\SetAlgoVlined
}
}
}
\Return{\texttt{false}}\;
}
\end{algorithm}
\begin{figure}
\begin{lstlisting}[
style=rustblock,
numbers=none,
label={listing:elision_in_rustc},
caption={
A simplified view of how finalizers are elided inside \ourgc. The new compiler intrinsic
\lstinline{needs_finalizer} returns true if a finalizer is required for a
type. The \lstinline{Gc<T>} type uses this intrinsic to ensure that the
value is registered as requiring a finalizer. With optimisations turned on, this
seemingly dynamic, branching code will be turned into static, branchless code.
}]
impl<T> Gc<T> {
pub fn new(value: T) -> Self {
if needs_finalizer::<T>() { Gc<T>::new_with_finalizer(value) }
else { Gc<T>::new_without_finalizer(value) }
...
}
}
\end{lstlisting}
\end{figure}
\subsection{Implementing Finalizer Elision}
\label{needs_finalizer_intrinsic}
Finalizer elision statically determines which type's destructors do
not require corresponding finalizers and elides them. It does so conservatively,
and deals correctly with drop glue.
\label{dropmethodfinalizerelidable}
As shown in \cref{alg:elision}, any type which implements
the \lstinline{Drop} trait requires finalization unless it also implements the
new \lstinline{DropMethodFinalizerElidable} \emph{marker trait} (i.e.~a
trait without methods). This trait can be
used by a programmer to signify that a type's drop method need
not be called if the type is placed inside a \lstinline{Gc<T>}. The `Drop'
part of the trait name is deliberate (i.e.~it is not a simplification of
`destructor') as it allows the programmer to reason about a type locally
(i.e.~without considering drop glue or concrete type paramaters).
If the type has a transitively reachable field
whose type implements the \lstinline{Drop} trait but not the
\lstinline{DropMethodFinalizerElidable} trait, then then the top-level type
still requires finalization.
Even though neither normal Rust destructors or \ourgc finalizers are guaranteed
to run, a program whose destructors or finalizers never run would probably not
be usable (leaking resources such as memory, deadlocking, and so on). We
therefore make \lstinline{DropMethodFinalizerElidable} an unsafe
trait, because implementing it inappropriately is likely to lead to undesired
-- though not incorrect! -- behaviour at run-time.
\ourgc modifies the standard Rust library to implement
\lstinline{DropMethodFinalizerElidable} on the following types: \lstinline{Box<T>},
\lstinline{Vec<T>}, \lstinline{RawVec<T>}, \lstinline{VecDeque<T>},
\lstinline{LinkedList<T>}, \lstinline{BTreeMap<K, V>}, \lstinline{BTreeSet<T>},
\lstinline{HashMap<K, V>}, \lstinline{HashSet<T>},
\lstinline{RawTable<K, V>}\footnote{This is a white lie, though the visible effect
is the same. \lstinline{RawTable} is contained in the separate \lstinline{hashbrown} crate
which is then included in Rust's standard library. We previously maintained a
fork of this, but synchronising it is painful. For now, at least, we have hacked explicit knowledge of \lstinline{RawTable}
into the \lstinline{needs_finalize} function.}, and \lstinline{BinaryHeap<T>}. Fortunately,
not only are these types' drop methods compatible with \lstinline{DropMethodFinalizerElidable},
but they are extensively used in real Rust code: they enable significant numbers of
finalizers to be elided.
\cref{listing:elision_in_rustc} shows the new \texttt{const} compiler intrinsic
\lstinline{needs_finalizer} we added to implement \cref{alg:elision}. The
intrinsic is evaluated at compile-time: its result can be inlined into \lstinline{Gc::new},
allowing the associated conditional to be removed too. In other words --
compiler optimisations allowing -- the `does this specific type require a
finalizer?' check has no run-time overhead.
\section{Premature Finalizer Prevention}
\label{sec:premature_finalize_prevention}
Most of us assume that finalizers are always run later than the
equivalent destructor would have run, but they can sometimes run
before~\cite[section~3.4]{boehm03destructors}, undermining soundness.
Such premature finalization is also possible in \ourgc as described thus far
(see~\cref{fig:premature_finalization}). In this section we
show how to prevent premature finalization.
There are two aspects to premature finalization. First, language
specifications often do not define, or do not precisely define, when the earliest point that a value can
be finalized is. While this means that, formally, there is no `premature' finalization,
it seems unlikely that language designers anticipated some of the resulting
implementation surprises (see e.g.~this example in
Java~\cite{shipilev20local}). Second, compiler optimisations -- at least in LLVM --
are `GC unaware', so optimisations such as scalar
replacement can change the point in a program when GCed values appear to be
finalizable.
\begin{figure}[t!]
\begin{lstlisting}[
style=rustblock,
caption={An example of possible premature
finalization. We create a new struct \lstinline{S}
(line 1) with a drop method that sets the wrapped integer to zero (line 2). In the
main method, we move an instance of the struct into a
\lstinline{Box<T>}, which we then move into a \lstinline{Gc<T>} (line 4). We
obtain a Rust reference to the inner integer (line 5), which at
run-time will be a pointer to the \lstinline{Box<T>}. At this
point, the compiler can determine that the \lstinline{Gc<T>} is no longer
used and overwrite \lstinline{root}'s pointer (which may be in a register). If
a collection then occurs, a finalizer can run \lstinline{S}'s drop method,
causing the program to print `0' instead of the expected `1' (line 7).},
label={fig:premature_finalization}]
struct S { value: u8 }
impl Drop for S { fn drop(&mut self) { self.value = 0; } }
fn main() {
let root = Gc::new(Box::new(S{ value: 1 }));
let inner: &u8 = &**root.value;
force_gc();
println!("{}", *inner);
}
\end{lstlisting}
\end{figure}
In our context, it is natural to define premature finalization as a (dynamic) finalizer
for a \lstinline{Gc<T>} value running before the (static) \lstinline{Gc<T>} owner
has gone out of scope. Similar to the high-level proposal mooted
in~\cite[Solution~1]{boehm07optimization}, we must ensure that the dynamic
lifetime of a reference derived from a \lstinline{Gc<T>} matches or
exceeds the lifetime of the \lstinline{Gc<T>} itself.
Our solution relies on adjusting \lstinline{Gc<T>}'s drop method to keep
alive a GCed value for at least the static lifetime of the \lstinline{Gc<T>} itself. In
other words, we ensure that the conservative GC will always see a pointer
to a GCed value while the corresponding \lstinline{Gc<T>} is in-scope.
However, there is a major problem to overcome: copyable types such as
\lstinline{Gc<T>} are forbidden from having destructors. The fundamental
challenge we have to solve is that each copied value will have a destructor
called on it, which has the potential for any shared underlying value to be destructed
more than once. \ourgc explicitly allows \lstinline{Gc<T>} -- but no other
copyable type -- to have a destructor, but to ensure it doesn't cause surprises
in the face of arbitrary numbers of copies, the destructor must be idempotent.
Our task is made easier because \lstinline{Gc<T>} naturally has no drop glue from
Rust's perspective: \lstinline{Gc<T>} consists of a field with a pointer type,
and such types are opaque from a destruction perspective.
We therefore only need to make sure that \lstinline{Gc<T>}'s drop method
is idempotent. Fortunately, this is sufficient for our purposes: we want the drop
method to inhibit finalization but that does not require run-time side effects.
To achieve this, we use a \emph{fence}. These come in various flavours. What
we need is a fence that prevents both: the compiler from reordering
computations around a particular syntactic point; and the CPU from reordering
computations around a particular address. We copy
the platform specific code from the \boehm \lstinline{GC_reachable_here}
macro\footnote{Initially we wrapped this macro in a function.
This led to surprising performance results: an analysis of LLVM IR suggests --
but the sheer quantity of data makes it hard to confirm -- that this may be
due to the effect on inlining heuristics.} into \lstinline{Gc<T>}'s
drop method, which achieves the effect we require.