Skip to content

Commit 08d2f70

Browse files
authored
Merge pull request #23610 from JuliaLang/kf/fancygcpreserve
Fancier GC preserve intrinsics
2 parents 6e95ea1 + d68e42f commit 08d2f70

13 files changed

+190
-30
lines changed

base/boot.jl

-2
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,4 @@ show(@nospecialize a) = show(STDOUT, a)
438438
print(@nospecialize a...) = print(STDOUT, a...)
439439
println(@nospecialize a...) = println(STDOUT, a...)
440440

441-
gcuse(@nospecialize a) = ccall(:jl_gc_use, Void, (Any,), a)
442-
443441
ccall(:jl_set_istopmod, Void, (Any, Bool), Core, true)

base/pointer.jl

+21
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,24 @@ isless(x::Ptr, y::Ptr) = isless(UInt(x), UInt(y))
150150
+(x::Ptr, y::Integer) = oftype(x, (UInt(x) + (y % UInt) % UInt))
151151
-(x::Ptr, y::Integer) = oftype(x, (UInt(x) - (y % UInt) % UInt))
152152
+(x::Integer, y::Ptr) = y + x
153+
154+
"""
155+
Temporarily protects an object from being garbage collected, even
156+
if it would otherwise be unreferenced.
157+
158+
The last argument is the expression to preserve objects during.
159+
The previous arguments are the objects to preserve.
160+
"""
161+
macro gc_preserve(args...)
162+
syms = args[1:end-1]
163+
for x in syms
164+
isa(x, Symbol) || error("Preserved variable must be a symbol")
165+
end
166+
s, r = gensym(), gensym()
167+
esc(quote
168+
$s = $(Expr(:gc_preserve_begin, syms...))
169+
$r = $(args[end])
170+
$(Expr(:gc_preserve_end, s))
171+
$r
172+
end)
173+
end

base/strings/string.jl

+1-4
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,7 @@ codeunit(s::AbstractString, i::Integer)
7373
@boundscheck if (i < 1) | (i > sizeof(s))
7474
throw(BoundsError(s,i))
7575
end
76-
ptr = pointer(s, i)
77-
r = unsafe_load(ptr)
78-
Core.gcuse(s)
79-
r
76+
@gc_preserve s unsafe_load(pointer(s, i))
8077
end
8178

8279
write(io::IO, s::String) = unsafe_write(io, pointer(s), reinterpret(UInt, sizeof(s)))

doc/src/devdocs/llvm.md

+21
Original file line numberDiff line numberDiff line change
@@ -297,3 +297,24 @@ for the function. As a result, the external rooting must be arranged while the
297297
value is still tracked by the system. I.e. it is not valid to attempt to use the
298298
result of this operation to establish a global root - the optimizer may have
299299
already dropped the value.
300+
301+
### Keeping values alive in the absence of uses
302+
303+
In certain cases it is necessary to keep an object alive, even though there is
304+
no compiler-visible use of said object. This may be case for low level code
305+
that operates on the memory-representation of an object directly or code that
306+
needs to interface with C code. In order to allow this, we provide the following
307+
intrinsics at the LLVM level:
308+
```
309+
token @llvm.julia.gc_preserve_begin(...)
310+
void @llvm.julia.gc_preserve_end(token)
311+
```
312+
(The `llvm.` in the name is required in order to be able to use the `token`
313+
type). The semantics of these intrinsics are as follows:
314+
At any safepoint that is dominated by a `gc_preserve_begin` call, but that is not
315+
not dominated by a corresponding `gc_preserve_end` call (i.e. a call whose argument
316+
is the token returned by a `gc_preserve_begin` call), the values passed as
317+
arguments to that `gc_preserve_begin` will be kept live. Note that the
318+
`gc_preserve_begin` still counts as a regular use of those values, so the
319+
standard lifetime semantics will ensure that the values will be kept alive
320+
before entering the preserve region.

src/ast.c

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ jl_sym_t *isdefined_sym; jl_sym_t *nospecialize_sym;
6060
jl_sym_t *macrocall_sym;
6161
jl_sym_t *hygienicscope_sym;
6262
jl_sym_t *escape_sym;
63+
jl_sym_t *gc_preserve_begin_sym; jl_sym_t *gc_preserve_end_sym;
6364

6465
static uint8_t flisp_system_image[] = {
6566
#include <julia_flisp.boot.inc>
@@ -340,6 +341,8 @@ void jl_init_frontend(void)
340341
macrocall_sym = jl_symbol("macrocall");
341342
escape_sym = jl_symbol("escape");
342343
hygienicscope_sym = jl_symbol("hygienic-scope");
344+
gc_preserve_begin_sym = jl_symbol("gc_preserve_begin");
345+
gc_preserve_end_sym = jl_symbol("gc_preserve_end");
343346
}
344347

345348
JL_DLLEXPORT void jl_lisp_prompt(void)

src/ccall.cpp

-7
Original file line numberDiff line numberDiff line change
@@ -1679,13 +1679,6 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs)
16791679
emit_signal_fence(ctx);
16801680
return ghostValue(jl_void_type);
16811681
}
1682-
else if (is_libjulia_func(jl_gc_use)) {
1683-
assert(lrt == T_void);
1684-
assert(!isVa && !llvmcall && nargt == 1);
1685-
ctx.builder.CreateCall(prepare_call(gc_use_func), {decay_derived(boxed(ctx, argv[0]))});
1686-
JL_GC_POP();
1687-
return ghostValue(jl_void_type);
1688-
}
16891682
else if (_is_libjulia_func((uintptr_t)ptls_getter, "jl_get_ptls_states")) {
16901683
assert(lrt == T_size);
16911684
assert(!isVa && !llvmcall && nargt == 0);

src/codegen.cpp

+48-5
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,8 @@ static GlobalVariable *jlgetworld_global;
356356

357357
// placeholder functions
358358
static Function *gcroot_flush_func;
359-
static Function *gc_use_func;
359+
static Function *gc_preserve_begin_func;
360+
static Function *gc_preserve_end_func;
360361
static Function *except_enter_func;
361362
static Function *pointer_from_objref_func;
362363

@@ -3905,6 +3906,42 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr)
39053906
else if (head == boundscheck_sym) {
39063907
return mark_julia_const(bounds_check_enabled(ctx, jl_true) ? jl_true : jl_false);
39073908
}
3909+
else if (head == gc_preserve_begin_sym) {
3910+
size_t nargs = jl_array_len(ex->args);
3911+
jl_cgval_t *argv = (jl_cgval_t*)alloca(sizeof(jl_cgval_t) * nargs);
3912+
for (size_t i = 0; i < nargs; ++i) {
3913+
argv[i] = emit_expr(ctx, args[i]);
3914+
}
3915+
size_t nargsboxed = 0;
3916+
Value **vals = (Value**)alloca(sizeof(Value *) * nargs);
3917+
for (size_t i = 0; i < nargs; ++i) {
3918+
if (!argv[i].isboxed) {
3919+
// This is intentionally not an error to allow writing
3920+
// generic code more easily.
3921+
continue;
3922+
} else if (argv[i].constant) {
3923+
continue;
3924+
}
3925+
vals[nargsboxed++] = argv[i].Vboxed;
3926+
}
3927+
Value *token = ctx.builder.CreateCall(prepare_call(gc_preserve_begin_func),
3928+
ArrayRef<Value*>(vals, nargsboxed));
3929+
jl_cgval_t tok(token, NULL, false, (jl_value_t*)jl_void_type, NULL);
3930+
tok.isimmutable = true;
3931+
return tok;
3932+
}
3933+
else if (head == gc_preserve_end_sym) {
3934+
// We only support ssa values as the argument. Everything else will
3935+
// fall back to the default behavior of preserving the argument value
3936+
// until the end of the scope, which is correct, but not optimal.
3937+
if (!jl_is_ssavalue(args[0])) {
3938+
return jl_cgval_t((jl_value_t*)jl_void_type);
3939+
}
3940+
jl_cgval_t token = emit_expr(ctx, args[0]);
3941+
assert(token.V->getType()->isTokenTy());
3942+
ctx.builder.CreateCall(prepare_call(gc_preserve_end_func), {token.V});
3943+
return jl_cgval_t((jl_value_t*)jl_void_type);
3944+
}
39083945
else {
39093946
if (!strcmp(jl_symbol_name(head), "$"))
39103947
jl_error("syntax: prefix \"$\" in non-quoted expression");
@@ -6526,11 +6563,17 @@ static void init_julia_llvm_env(Module *m)
65266563
"julia.gcroot_flush");
65276564
add_named_global(gcroot_flush_func, (void*)NULL, /*dllimport*/false);
65286565

6529-
gc_use_func = Function::Create(FunctionType::get(T_void,
6530-
ArrayRef<Type*>(PointerType::get(T_jlvalue, AddressSpace::Derived)), false),
6566+
gc_preserve_begin_func = Function::Create(FunctionType::get(Type::getTokenTy(jl_LLVMContext),
6567+
ArrayRef<Type*>(), true),
65316568
Function::ExternalLinkage,
6532-
"julia.gc_use");
6533-
add_named_global(gc_use_func, (void*)NULL, /*dllimport*/false);
6569+
"llvm.julia.gc_preserve_begin");
6570+
add_named_global(gc_preserve_begin_func, (void*)NULL, /*dllimport*/false);
6571+
6572+
gc_preserve_end_func = Function::Create(FunctionType::get(T_void,
6573+
ArrayRef<Type*>(Type::getTokenTy(jl_LLVMContext)), false),
6574+
Function::ExternalLinkage,
6575+
"llvm.julia.gc_preserve_end");
6576+
add_named_global(gc_preserve_end_func, (void*)NULL, /*dllimport*/false);
65346577

65356578
pointer_from_objref_func = Function::Create(FunctionType::get(T_size,
65366579
ArrayRef<Type*>(PointerType::get(T_jlvalue, AddressSpace::Derived)), false),

src/interpreter.c

+5
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,11 @@ static jl_value_t *eval(jl_value_t *e, interpreter_state *s)
506506
else if (ex->head == boundscheck_sym || ex->head == inbounds_sym || ex->head == fastmath_sym ||
507507
ex->head == simdloop_sym || ex->head == meta_sym) {
508508
return jl_nothing;
509+
} else if (ex->head == gc_preserve_begin_sym || ex->head == gc_preserve_end_sym) {
510+
// The interpreter generally keeps values that were assigned in this scope
511+
// rooted. If the interpreter learns to be more agressive here, we may
512+
// want to explicitly root these values.
513+
return jl_nothing;
509514
}
510515
jl_errorf("unsupported or misplaced expression %s", jl_symbol_name(ex->head));
511516
abort();

src/julia-syntax.scm

+8-2
Original file line numberDiff line numberDiff line change
@@ -3084,7 +3084,8 @@ f(x) = yt(x)
30843084
(memq (car e) '(quote top core line inert local local-def unnecessary
30853085
meta inbounds boundscheck simdloop decl
30863086
implicit-global global globalref outerref
3087-
const = null method call foreigncall ssavalue))))
3087+
const = null method call foreigncall ssavalue
3088+
gc_preserve_begin gc_preserve_end))))
30883089
(lam:body lam))))
30893090
(unused (map cadr (filter (lambda (x) (memq (car x) '(method =)))
30903091
leading))))
@@ -3816,8 +3817,13 @@ f(x) = yt(x)
38163817
(if tail (emit-return '(null)))
38173818
'(null))
38183819

3820+
((gc_preserve_begin)
3821+
(let ((s (make-ssavalue)))
3822+
(emit `(= ,s ,e))
3823+
s))
3824+
38193825
;; other top level expressions and metadata
3820-
((import importall using export line meta inbounds boundscheck simdloop)
3826+
((import importall using export line meta inbounds boundscheck simdloop gc_preserve_end)
38213827
(let ((have-ret? (and (pair? code) (pair? (car code)) (eq? (caar code) 'return))))
38223828
(cond ((eq? (car e) 'line)
38233829
(set! current-loc e)

src/julia_internal.h

+1
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ extern jl_sym_t *propagate_inbounds_sym;
999999
extern jl_sym_t *isdefined_sym;
10001000
extern jl_sym_t *nospecialize_sym;
10011001
extern jl_sym_t *boundscheck_sym;
1002+
extern jl_sym_t *gc_preserve_begin_sym; extern jl_sym_t *gc_preserve_end_sym;
10021003

10031004
void jl_register_fptrs(uint64_t sysimage_base, const char *base, const int32_t *offsets,
10041005
jl_method_instance_t **linfos, size_t n);

src/llvm-late-gc-lowering.cpp

+58-9
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ struct State {
259259
// of the value (but not the other way around).
260260
std::map<int, int> LoadRefinements;
261261

262+
// GC preserves map. All safepoints dominated by the map key, but not any
263+
// of its uses need to preserve the values listed in the map value.
264+
std::map<Instruction *, std::vector<int>> GCPreserves;
265+
262266
// The assignment of numbers to safepoints. The indices in the map
263267
// are indices into the next three maps which store safepoint properties
264268
std::map<Instruction *, int> SafepointNumbering;
@@ -322,7 +326,8 @@ struct LateLowerGCFrame: public FunctionPass {
322326
MDNode *tbaa_tag;
323327
Function *ptls_getter;
324328
Function *gc_flush_func;
325-
Function *gc_use_func;
329+
Function *gc_preserve_begin_func;
330+
Function *gc_preserve_end_func;
326331
Function *pointer_from_objref_func;
327332
Function *alloc_obj_func;
328333
Function *pool_alloc_func;
@@ -754,17 +759,29 @@ State LateLowerGCFrame::LocalScan(Function &F) {
754759
if (CI->canReturnTwice()) {
755760
S.ReturnsTwice.push_back(CI);
756761
}
757-
if (isa<IntrinsicInst>(CI)) {
758-
// Intrinsics are never safepoints.
759-
continue;
760-
}
761762
if (auto callee = CI->getCalledFunction()) {
763+
if (callee == gc_preserve_begin_func) {
764+
std::vector<int> args;
765+
for (Use &U : CI->arg_operands()) {
766+
Value *V = U;
767+
int Num = Number(S, V);
768+
if (Num >= 0)
769+
args.push_back(Num);
770+
}
771+
S.GCPreserves[CI] = args;
772+
continue;
773+
}
762774
// Known functions emitted in codegen that are not safepoints
763-
if (callee == pointer_from_objref_func || callee == gc_use_func ||
775+
if (callee == pointer_from_objref_func || callee == gc_preserve_begin_func ||
776+
callee == gc_preserve_end_func ||
764777
callee->getName() == "memcmp") {
765778
continue;
766779
}
767780
}
781+
if (isa<IntrinsicInst>(CI)) {
782+
// Intrinsics are never safepoints.
783+
continue;
784+
}
768785
int SafepointNumber = NoteSafepoint(S, BBS, CI);
769786
BBS.HasSafepoint = true;
770787
BBS.TopmostSafepoint = SafepointNumber;
@@ -912,6 +929,7 @@ JL_USED_FUNC static void dumpSafepointsForBBName(Function &F, State &S, const ch
912929
}
913930

914931
void LateLowerGCFrame::ComputeLiveSets(Function &F, State &S) {
932+
DominatorTree *DT = nullptr;
915933
// Iterate over all safe points. Add to live sets all those variables that
916934
// are now live across their parent block.
917935
for (auto it : S.SafepointNumbering) {
@@ -935,6 +953,33 @@ void LateLowerGCFrame::ComputeLiveSets(Function &F, State &S) {
935953
if (RefinedPtr == -1 || HasBitSet(LS, RefinedPtr))
936954
LS[Idx] = 0;
937955
}
956+
// If the function has GC preserves, figure out whether we need to
957+
// add in any extra live values.
958+
if (!S.GCPreserves.empty()) {
959+
if (!DT) {
960+
DT = &getAnalysis<DominatorTreeWrapperPass>().getDomTree();
961+
}
962+
for (auto it2 : S.GCPreserves) {
963+
if (!DT->dominates(it2.first, Safepoint))
964+
continue;
965+
bool OutsideRange = false;
966+
for (const User *U : it2.first->users()) {
967+
// If this is dominated by an end, we don't need to add
968+
// the values to our live set.
969+
if (DT->dominates(cast<Instruction>(U), Safepoint)) {
970+
OutsideRange = true;
971+
break;
972+
}
973+
}
974+
if (OutsideRange)
975+
continue;
976+
for (unsigned Num : it2.second) {
977+
if (Num >= LS.size())
978+
LS.resize(Num + 1);
979+
LS[Num] = 1;
980+
}
981+
}
982+
}
938983
}
939984
// Compute the interference graph
940985
for (int i = 0; i <= S.MaxPtrNumber; ++i) {
@@ -1153,8 +1198,8 @@ bool LateLowerGCFrame::CleanupIR(Function &F) {
11531198
}
11541199
CallingConv::ID CC = CI->getCallingConv();
11551200
auto callee = CI->getCalledValue();
1156-
if ((gc_flush_func != nullptr && callee == gc_flush_func) ||
1157-
(gc_use_func != nullptr && callee == gc_use_func)) {
1201+
if (callee && (callee == gc_flush_func || callee == gc_preserve_begin_func
1202+
|| callee == gc_preserve_end_func)) {
11581203
/* No replacement */
11591204
} else if (pointer_from_objref_func != nullptr && callee == pointer_from_objref_func) {
11601205
auto *obj = CI->getOperand(0);
@@ -1244,6 +1289,9 @@ bool LateLowerGCFrame::CleanupIR(Function &F) {
12441289
NewCall->takeName(CI);
12451290
CI->replaceAllUsesWith(NewCall);
12461291
}
1292+
if (!CI->use_empty()) {
1293+
CI->replaceAllUsesWith(UndefValue::get(CI->getType()));
1294+
}
12471295
it = CI->eraseFromParent();
12481296
ChangesMade = true;
12491297
}
@@ -1423,7 +1471,8 @@ static void addRetNoAlias(Function *F)
14231471
bool LateLowerGCFrame::DefineFunctions(Module &M) {
14241472
ptls_getter = M.getFunction("jl_get_ptls_states");
14251473
gc_flush_func = M.getFunction("julia.gcroot_flush");
1426-
gc_use_func = M.getFunction("julia.gc_use");
1474+
gc_preserve_begin_func = M.getFunction("llvm.julia.gc_preserve_begin");
1475+
gc_preserve_end_func = M.getFunction("llvm.julia.gc_preserve_end");
14271476
pointer_from_objref_func = M.getFunction("julia.pointer_from_objref");
14281477
auto &ctx = M.getContext();
14291478
T_size = M.getDataLayout().getIntPtrType(ctx);

src/macroexpand.scm

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@
307307
,(resolve-expansion-vars-with-new-env (caddr arg) env m inarg))))
308308
(else
309309
`(global ,(resolve-expansion-vars-with-new-env arg env m inarg))))))
310-
((using import importall export meta line inbounds boundscheck simdloop) (map unescape e))
310+
((using import importall export meta line inbounds boundscheck simdloop gc_preserve gc_preserve_end) (map unescape e))
311311
((macrocall) e) ; invalid syntax anyways, so just act like it's quoted.
312312
((symboliclabel) e)
313313
((symbolicgoto) e)

test/llvmpasses/gcroots.ll

+23
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,26 @@ top:
223223
ret void
224224
}
225225

226+
declare token @llvm.julia.gc_preserve_begin(...)
227+
declare void @llvm.julia.gc_preserve_end(token)
228+
229+
define void @gc_preserve(i64 %a) {
230+
; CHECK-LABEL: @gc_preserve
231+
; CHECK: %gcframe = alloca %jl_value_t addrspace(10)*, i32 4
232+
top:
233+
%ptls = call %jl_value_t*** @jl_get_ptls_states()
234+
%aboxed = call %jl_value_t addrspace(10)* @jl_box_int64(i64 signext %a)
235+
; CHECK: store %jl_value_t addrspace(10)* %aboxed
236+
call void @jl_safepoint()
237+
%tok = call token (...) @llvm.julia.gc_preserve_begin(%jl_value_t addrspace(10)* %aboxed)
238+
%aboxed2 = call %jl_value_t addrspace(10)* @jl_box_int64(i64 signext %a)
239+
; CHECK: store %jl_value_t addrspace(10)* %aboxed2
240+
call void @jl_safepoint()
241+
call void @llvm.julia.gc_preserve_end(token %tok)
242+
%aboxed3 = call %jl_value_t addrspace(10)* @jl_box_int64(i64 signext %a)
243+
; CHECK: store %jl_value_t addrspace(10)* %aboxed3
244+
call void @jl_safepoint()
245+
call void @one_arg_boxed(%jl_value_t addrspace(10)* %aboxed2)
246+
call void @one_arg_boxed(%jl_value_t addrspace(10)* %aboxed3)
247+
ret void
248+
}

0 commit comments

Comments
 (0)