Skip to content

Commit 7db327e

Browse files
committed
Fancier GC preserve intrinsics
1 parent 508c9f5 commit 7db327e

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
@@ -57,6 +57,7 @@ jl_sym_t *unused_sym; jl_sym_t *static_parameter_sym;
5757
jl_sym_t *polly_sym; jl_sym_t *inline_sym;
5858
jl_sym_t *propagate_inbounds_sym;
5959
jl_sym_t *isdefined_sym; jl_sym_t *nospecialize_sym;
60+
jl_sym_t *gc_preserve_begin_sym; jl_sym_t *gc_preserve_end_sym;
6061

6162
static uint8_t flisp_system_image[] = {
6263
#include <julia_flisp.boot.inc>
@@ -437,6 +438,8 @@ void jl_init_frontend(void)
437438
propagate_inbounds_sym = jl_symbol("propagate_inbounds");
438439
isdefined_sym = jl_symbol("isdefined");
439440
nospecialize_sym = jl_symbol("nospecialize");
441+
gc_preserve_begin_sym = jl_symbol("gc_preserve_begin");
442+
gc_preserve_end_sym = jl_symbol("gc_preserve_end");
440443
}
441444

442445
JL_DLLEXPORT void jl_lisp_prompt(void)

src/ccall.cpp

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

@@ -3843,6 +3844,42 @@ static jl_cgval_t emit_expr(jl_codectx_t &ctx, jl_value_t *expr)
38433844
else if (head == boundscheck_sym) {
38443845
return mark_julia_const(bounds_check_enabled(ctx, jl_true) ? jl_true : jl_false);
38453846
}
3847+
else if (head == gc_preserve_begin_sym) {
3848+
size_t nargs = jl_array_len(ex->args);
3849+
jl_cgval_t *argv = (jl_cgval_t*)alloca(sizeof(jl_cgval_t) * nargs);
3850+
for (size_t i = 0; i < nargs; ++i) {
3851+
argv[i] = emit_expr(ctx, args[i]);
3852+
}
3853+
size_t nargsboxed = 0;
3854+
Value **vals = (Value**)alloca(sizeof(Value *) * nargs);
3855+
for (size_t i = 0; i < nargs; ++i) {
3856+
if (!argv[i].isboxed) {
3857+
// This is intentionally not an error to allow writing
3858+
// generic code more easily.
3859+
continue;
3860+
} else if (argv[i].constant) {
3861+
continue;
3862+
}
3863+
vals[nargsboxed++] = argv[i].Vboxed;
3864+
}
3865+
Value *token = ctx.builder.CreateCall(prepare_call(gc_preserve_begin_func),
3866+
ArrayRef<Value*>(vals, nargsboxed));
3867+
jl_cgval_t tok(token, NULL, false, (jl_value_t*)jl_void_type, NULL);
3868+
tok.isimmutable = true;
3869+
return tok;
3870+
}
3871+
else if (head == gc_preserve_end_sym) {
3872+
// We only support ssa values as the argument. Everything else will
3873+
// fall back to the default behavior of preserving the argument value
3874+
// until the end of the scope, which is correct, but not optimal.
3875+
if (!jl_is_ssavalue(args[0])) {
3876+
return jl_cgval_t((jl_value_t*)jl_void_type);
3877+
}
3878+
jl_cgval_t token = emit_expr(ctx, args[0]);
3879+
assert(token.V->getType()->isTokenTy());
3880+
ctx.builder.CreateCall(prepare_call(gc_preserve_end_func), {token.V});
3881+
return jl_cgval_t((jl_value_t*)jl_void_type);
3882+
}
38463883
else {
38473884
if (!strcmp(jl_symbol_name(head), "$"))
38483885
jl_error("syntax: prefix \"$\" in non-quoted expression");
@@ -6421,11 +6458,17 @@ static void init_julia_llvm_env(Module *m)
64216458
"julia.gcroot_flush");
64226459
add_named_global(gcroot_flush_func, (void*)NULL, /*dllimport*/false);
64236460

6424-
gc_use_func = Function::Create(FunctionType::get(T_void,
6425-
ArrayRef<Type*>(PointerType::get(T_jlvalue, AddressSpace::Derived)), false),
6461+
gc_preserve_begin_func = Function::Create(FunctionType::get(Type::getTokenTy(jl_LLVMContext),
6462+
ArrayRef<Type*>(), true),
64266463
Function::ExternalLinkage,
6427-
"julia.gc_use");
6428-
add_named_global(gc_use_func, (void*)NULL, /*dllimport*/false);
6464+
"llvm.julia.gc_preserve_begin");
6465+
add_named_global(gc_preserve_begin_func, (void*)NULL, /*dllimport*/false);
6466+
6467+
gc_preserve_end_func = Function::Create(FunctionType::get(T_void,
6468+
ArrayRef<Type*>(Type::getTokenTy(jl_LLVMContext)), false),
6469+
Function::ExternalLinkage,
6470+
"llvm.julia.gc_preserve_end");
6471+
add_named_global(gc_preserve_end_func, (void*)NULL, /*dllimport*/false);
64296472

64306473
pointer_from_objref_func = Function::Create(FunctionType::get(T_pjlvalue,
64316474
ArrayRef<Type*>(PointerType::get(T_jlvalue, AddressSpace::Derived)), false),

src/interpreter.c

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

src/julia-syntax.scm

+8-2
Original file line numberDiff line numberDiff line change
@@ -3079,7 +3079,8 @@ f(x) = yt(x)
30793079
(memq (car e) '(quote top core line inert local local-def unnecessary
30803080
meta inbounds boundscheck simdloop decl
30813081
implicit-global global globalref outerref
3082-
const = null method call foreigncall ssavalue))))
3082+
const = null method call foreigncall ssavalue
3083+
gc_preserve_begin gc_preserve_end))))
30833084
(lam:body lam))))
30843085
(unused (map cadr (filter (lambda (x) (memq (car x) '(method =)))
30853086
leading))))
@@ -3797,8 +3798,13 @@ f(x) = yt(x)
37973798
(if tail (emit-return '(null)))
37983799
'(null))
37993800

3801+
((gc_preserve_begin)
3802+
(let ((s (make-ssavalue)))
3803+
(emit `(= ,s ,e))
3804+
s))
3805+
38003806
;; other top level expressions and metadata
3801-
((import importall using export line meta inbounds boundscheck simdloop)
3807+
((import importall using export line meta inbounds boundscheck simdloop gc_preserve_end)
38023808
(let ((have-ret? (and (pair? code) (pair? (car code)) (eq? (caar code) 'return))))
38033809
(cond ((eq? (car e) 'line)
38043810
(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;
@@ -321,7 +325,8 @@ struct LateLowerGCFrame: public FunctionPass {
321325
MDNode *tbaa_tag;
322326
Function *ptls_getter;
323327
Function *gc_flush_func;
324-
Function *gc_use_func;
328+
Function *gc_preserve_begin_func;
329+
Function *gc_preserve_end_func;
325330
Function *pointer_from_objref_func;
326331
Function *alloc_obj_func;
327332
Function *pool_alloc_func;
@@ -753,17 +758,29 @@ State LateLowerGCFrame::LocalScan(Function &F) {
753758
if (CI->canReturnTwice()) {
754759
S.ReturnsTwice.push_back(CI);
755760
}
756-
if (isa<IntrinsicInst>(CI)) {
757-
// Intrinsics are never safepoints.
758-
continue;
759-
}
760761
if (auto callee = CI->getCalledFunction()) {
762+
if (callee == gc_preserve_begin_func) {
763+
std::vector<int> args;
764+
for (Use &U : CI->arg_operands()) {
765+
Value *V = U;
766+
int Num = Number(S, V);
767+
if (Num >= 0)
768+
args.push_back(Num);
769+
}
770+
S.GCPreserves[CI] = args;
771+
continue;
772+
}
761773
// Known functions emitted in codegen that are not safepoints
762-
if (callee == pointer_from_objref_func || callee == gc_use_func ||
774+
if (callee == pointer_from_objref_func || callee == gc_preserve_begin_func ||
775+
callee == gc_preserve_end_func ||
763776
callee->getName() == "memcmp") {
764777
continue;
765778
}
766779
}
780+
if (isa<IntrinsicInst>(CI)) {
781+
// Intrinsics are never safepoints.
782+
continue;
783+
}
767784
int SafepointNumber = NoteSafepoint(S, BBS, CI);
768785
BBS.HasSafepoint = true;
769786
BBS.TopmostSafepoint = SafepointNumber;
@@ -911,6 +928,7 @@ JL_USED_FUNC static void dumpSafepointsForBBName(Function &F, State &S, const ch
911928
}
912929

913930
void LateLowerGCFrame::ComputeLiveSets(Function &F, State &S) {
931+
DominatorTree *DT = nullptr;
914932
// Iterate over all safe points. Add to live sets all those variables that
915933
// are now live across their parent block.
916934
for (auto it : S.SafepointNumbering) {
@@ -934,6 +952,33 @@ void LateLowerGCFrame::ComputeLiveSets(Function &F, State &S) {
934952
if (RefinedPtr == -1 || HasBitSet(LS, RefinedPtr))
935953
LS[Idx] = 0;
936954
}
955+
// If the function has GC preserves, figure out whether we need to
956+
// add in any extra live values.
957+
if (!S.GCPreserves.empty()) {
958+
if (!DT) {
959+
DT = &getAnalysis<DominatorTreeWrapperPass>().getDomTree();
960+
}
961+
for (auto it2 : S.GCPreserves) {
962+
if (!DT->dominates(it2.first, Safepoint))
963+
continue;
964+
bool OutsideRange = false;
965+
for (const User *U : it2.first->users()) {
966+
// If this is dominated by an end, we don't need to add
967+
// the values to our live set.
968+
if (DT->dominates(cast<Instruction>(U), Safepoint)) {
969+
OutsideRange = true;
970+
break;
971+
}
972+
}
973+
if (OutsideRange)
974+
continue;
975+
for (unsigned Num : it2.second) {
976+
if (Num >= LS.size())
977+
LS.resize(Num + 1);
978+
LS[Num] = 1;
979+
}
980+
}
981+
}
937982
}
938983
// Compute the interference graph
939984
for (int i = 0; i <= S.MaxPtrNumber; ++i) {
@@ -1152,8 +1197,8 @@ bool LateLowerGCFrame::CleanupIR(Function &F) {
11521197
}
11531198
CallingConv::ID CC = CI->getCallingConv();
11541199
auto callee = CI->getCalledValue();
1155-
if ((gc_flush_func != nullptr && callee == gc_flush_func) ||
1156-
(gc_use_func != nullptr && callee == gc_use_func)) {
1200+
if (callee && (callee == gc_flush_func || callee == gc_preserve_begin_func
1201+
|| callee == gc_preserve_end_func)) {
11571202
/* No replacement */
11581203
} else if (pointer_from_objref_func != nullptr && callee == pointer_from_objref_func) {
11591204
auto *ASCI = new AddrSpaceCastInst(CI->getOperand(0),
@@ -1242,6 +1287,9 @@ bool LateLowerGCFrame::CleanupIR(Function &F) {
12421287
NewCall->takeName(CI);
12431288
CI->replaceAllUsesWith(NewCall);
12441289
}
1290+
if (!CI->use_empty()) {
1291+
CI->replaceAllUsesWith(UndefValue::get(CI->getType()));
1292+
}
12451293
it = CI->eraseFromParent();
12461294
ChangesMade = true;
12471295
}
@@ -1421,7 +1469,8 @@ static void addRetNoAlias(Function *F)
14211469
bool LateLowerGCFrame::doInitialization(Module &M) {
14221470
ptls_getter = M.getFunction("jl_get_ptls_states");
14231471
gc_flush_func = M.getFunction("julia.gcroot_flush");
1424-
gc_use_func = M.getFunction("julia.gc_use");
1472+
gc_preserve_begin_func = M.getFunction("llvm.julia.gc_preserve_begin");
1473+
gc_preserve_end_func = M.getFunction("llvm.julia.gc_preserve_end");
14251474
pointer_from_objref_func = M.getFunction("julia.pointer_from_objref");
14261475
auto &ctx = M.getContext();
14271476
T_size = M.getDataLayout().getIntPtrType(ctx);

src/macroexpand.scm

+1-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@
311311
,(resolve-expansion-vars-with-new-env (caddr arg) env m inarg))))
312312
(else
313313
`(global ,(resolve-expansion-vars-with-new-env arg env m inarg))))))
314-
((using import importall export meta line inbounds boundscheck simdloop) (map unescape e))
314+
((using import importall export meta line inbounds boundscheck simdloop gc_preserve gc_preserve_end) (map unescape e))
315315
((macrocall) e) ; invalid syntax anyways, so just act like it's quoted.
316316
((symboliclabel) e)
317317
((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)