Skip to content

Commit 5da257d

Browse files
vchuravygbaraldi
authored andcommitted
Allow for :foreigncall to transition to GC safe automatically (#49933)
This has been bouncing around as a idea for a while. One of the challenges around time-to-safepoint has been Julia code that is calling libraries. Since foreign code will not include safepoints we see increased latency when one thread is running a foreign-call and another wants to trigger GC. The open design question here is: - Do we expose this as an option the user must "opt-in", e.g. by using a keyword arg to `@ccall` or a specific calling-convetion. - Or do we turn this on for all ccall, except for Julia runtime calls. There is relativly little code outside the Julia runtime that needs to be "GC unsafe", exception are programs that directly use the Julia C-API. Incidentially `jl_adopt_thread` and `@cfunction`/`@ccallable` do the right thing and transition to "GC unsafe", regardless of what state the thread currently is in. I still need to figure out how to reliably detect Julia runtime calls, but I think we can switch all other calls to "GC safe". We should also consider optimizations that mark large regions of code without Julia runtime interactions as "GC safe" in particular numeric for-loops. Closes #57057 --------- Co-authored-by: Gabriel Baraldi <baraldigabriel@gmail.com> (cherry picked from commit 85458a0)
1 parent 423cb56 commit 5da257d

File tree

16 files changed

+160
-44
lines changed

16 files changed

+160
-44
lines changed

Compiler/src/abstractinterpretation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3409,7 +3409,7 @@ function abstract_eval_foreigncall(interp::AbstractInterpreter, e::Expr, sstate:
34093409
abstract_eval_value(interp, x, sstate, sv)
34103410
end
34113411
cconv = e.args[5]
3412-
if isa(cconv, QuoteNode) && (v = cconv.value; isa(v, Tuple{Symbol, UInt16}))
3412+
if isa(cconv, QuoteNode) && (v = cconv.value; isa(v, Tuple{Symbol, UInt16, Bool}))
34133413
override = decode_effects_override(v[2])
34143414
effects = override_effects(effects, override)
34153415
end

Compiler/src/validation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const VALID_EXPR_HEADS = IdDict{Symbol,UnitRange{Int}}(
2323
:meta => 0:typemax(Int),
2424
:global => 1:1,
2525
:globaldecl => 1:2,
26-
:foreigncall => 5:typemax(Int), # name, RT, AT, nreq, (cconv, effects), args..., roots...
26+
:foreigncall => 5:typemax(Int), # name, RT, AT, nreq, (cconv, effects, gc_safe), args..., roots...
2727
:cfunction => 5:5,
2828
:isdefined => 1:2,
2929
:code_coverage_effect => 0:0,

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ New language features
2525
* Support for Unicode 16 ([#56925]).
2626
* `Threads.@spawn` now takes a `:samepool` argument to specify the same threadpool as the caller.
2727
`Threads.@spawn :samepool foo()` which is shorthand for `Threads.@spawn Threads.threadpool() foo()` ([#57109]).
28+
* The `@ccall` macro can now take a `gc_safe` argument, that if set to true allows the runtime to run garbage collection concurrently to the `ccall`
2829

2930
Language changes
3031
----------------

base/c.jl

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,31 @@ The above input outputs this:
268268
269269
(:printf, :Cvoid, [:Cstring, :Cuint], ["%d", :value])
270270
"""
271-
function ccall_macro_parse(expr::Expr)
271+
function ccall_macro_parse(exprs)
272+
gc_safe = false
273+
expr = nothing
274+
if exprs isa Expr
275+
expr = exprs
276+
elseif length(exprs) == 1
277+
expr = exprs[1]
278+
elseif length(exprs) == 2
279+
gc_expr = exprs[1]
280+
expr = exprs[2]
281+
if gc_expr.head == :(=) && gc_expr.args[1] == :gc_safe
282+
if gc_expr.args[2] == true
283+
gc_safe = true
284+
elseif gc_expr.args[2] == false
285+
gc_safe = false
286+
else
287+
throw(ArgumentError("gc_safe must be true or false"))
288+
end
289+
else
290+
throw(ArgumentError("@ccall option must be `gc_safe=true` or `gc_safe=false`"))
291+
end
292+
else
293+
throw(ArgumentError("@ccall needs a function signature with a return type"))
294+
end
295+
272296
# setup and check for errors
273297
if !isexpr(expr, :(::))
274298
throw(ArgumentError("@ccall needs a function signature with a return type"))
@@ -328,12 +352,11 @@ function ccall_macro_parse(expr::Expr)
328352
pusharg!(a)
329353
end
330354
end
331-
332-
return func, rettype, types, args, nreq
355+
return func, rettype, types, args, gc_safe, nreq
333356
end
334357

335358

336-
function ccall_macro_lower(convention, func, rettype, types, args, nreq)
359+
function ccall_macro_lower(convention, func, rettype, types, args, gc_safe, nreq)
337360
statements = []
338361

339362
# if interpolation was used, ensure the value is a function pointer at runtime.
@@ -351,9 +374,15 @@ function ccall_macro_lower(convention, func, rettype, types, args, nreq)
351374
else
352375
func = esc(func)
353376
end
377+
cconv = nothing
378+
if convention isa Tuple
379+
cconv = Expr(:cconv, (convention..., gc_safe), nreq)
380+
else
381+
cconv = Expr(:cconv, (convention, UInt16(0), gc_safe), nreq)
382+
end
354383

355384
return Expr(:block, statements...,
356-
Expr(:call, :ccall, func, Expr(:cconv, convention, nreq), esc(rettype),
385+
Expr(:call, :ccall, func, cconv, esc(rettype),
357386
Expr(:tuple, map(esc, types)...), map(esc, args)...))
358387
end
359388

@@ -404,9 +433,16 @@ Example using an external library:
404433
405434
The string literal could also be used directly before the function
406435
name, if desired `"libglib-2.0".g_uri_escape_string(...`
436+
437+
It's possible to declare the ccall as `gc_safe` by using the `gc_safe = true` option:
438+
@ccall gc_safe=true strlen(s::Cstring)::Csize_t
439+
This allows the garbage collector to run concurrently with the ccall, which can be useful whenever
440+
the `ccall` may block outside of julia.
441+
WARNING: This option should be used with caution, as it can lead to undefined behavior if the ccall
442+
calls back into the julia runtime. (`@cfunction`/`@ccallables` are safe however)
407443
"""
408-
macro ccall(expr)
409-
return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...)
444+
macro ccall(exprs...)
445+
return ccall_macro_lower((:ccall), ccall_macro_parse(exprs)...)
410446
end
411447

412448
macro ccall_effects(effects::UInt16, expr)

base/meta.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ function _partially_inline!(@nospecialize(x), slot_replacements::Vector{Any},
427427
elseif i == 4
428428
@assert isa(x.args[4], Int)
429429
elseif i == 5
430-
@assert isa((x.args[5]::QuoteNode).value, Union{Symbol, Tuple{Symbol, UInt8}})
430+
@assert isa((x.args[5]::QuoteNode).value, Union{Symbol, Tuple{Symbol, UInt16, Bool}})
431431
else
432432
x.args[i] = _partially_inline!(x.args[i], slot_replacements,
433433
type_signature, static_param_values,

base/strings/string.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ end
107107
# but the macro is not available at this time in bootstrap, so we write it manually.
108108
const _string_n_override = 0x04ee
109109
@eval _string_n(n::Integer) = $(Expr(:foreigncall, QuoteNode(:jl_alloc_string), Ref{String},
110-
:(Core.svec(Csize_t)), 1, QuoteNode((:ccall, _string_n_override)), :(convert(Csize_t, n))))
110+
:(Core.svec(Csize_t)), 1, QuoteNode((:ccall, _string_n_override, false)), :(convert(Csize_t, n))))
111111

112112
"""
113113
String(s::AbstractString)

doc/src/devdocs/ast.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,9 @@ These symbols appear in the `head` field of [`Expr`](@ref)s in lowered form.
498498

499499
The number of required arguments for a varargs function definition.
500500

501-
* `args[5]::QuoteNode{<:Union{Symbol,Tuple{Symbol,UInt16}}`: calling convention
501+
* `args[5]::QuoteNode{<:Union{Symbol,Tuple{Symbol,UInt16}, Tuple{Symbol,UInt16,Bool}}`: calling convention
502502

503-
The calling convention for the call, optionally with effects.
503+
The calling convention for the call, optionally with effects, and `gc_safe` (safe to execute concurrently to GC.).
504504

505505
* `args[6:5+length(args[3])]` : arguments
506506

src/ccall.cpp

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,7 @@ class function_sig_t {
11341134
AttributeList attributes; // vector of function call site attributes
11351135
Type *lrt; // input parameter of the llvm return type (from julia_struct_to_llvm)
11361136
bool retboxed; // input parameter indicating whether lrt is jl_value_t*
1137+
bool gc_safe; // input parameter indicating whether the call is safe to execute concurrently to GC
11371138
Type *prt; // out parameter of the llvm return type for the function signature
11381139
int sret; // out parameter for indicating whether return value has been moved to the first argument position
11391140
std::string err_msg;
@@ -1146,8 +1147,8 @@ class function_sig_t {
11461147
size_t nreqargs; // number of required arguments in ccall function definition
11471148
jl_codegen_params_t *ctx;
11481149

1149-
function_sig_t(const char *fname, Type *lrt, jl_value_t *rt, bool retboxed, jl_svec_t *at, jl_unionall_t *unionall_env, size_t nreqargs, CallingConv::ID cc, bool llvmcall, jl_codegen_params_t *ctx)
1150-
: lrt(lrt), retboxed(retboxed),
1150+
function_sig_t(const char *fname, Type *lrt, jl_value_t *rt, bool retboxed, bool gc_safe, jl_svec_t *at, jl_unionall_t *unionall_env, size_t nreqargs, CallingConv::ID cc, bool llvmcall, jl_codegen_params_t *ctx)
1151+
: lrt(lrt), retboxed(retboxed), gc_safe(gc_safe),
11511152
prt(NULL), sret(0), cc(cc), llvmcall(llvmcall),
11521153
at(at), rt(rt), unionall_env(unionall_env),
11531154
nccallargs(jl_svec_len(at)), nreqargs(nreqargs),
@@ -1295,6 +1296,7 @@ std::string generate_func_sig(const char *fname)
12951296
RetAttrs = RetAttrs.addAttribute(LLVMCtx, Attribute::NonNull);
12961297
if (rt == jl_bottom_type)
12971298
FnAttrs = FnAttrs.addAttribute(LLVMCtx, Attribute::NoReturn);
1299+
12981300
assert(attributes.isEmpty());
12991301
attributes = AttributeList::get(LLVMCtx, FnAttrs, RetAttrs, paramattrs);
13001302
return "";
@@ -1412,7 +1414,7 @@ static const std::string verify_ccall_sig(jl_value_t *&rt, jl_value_t *at,
14121414

14131415
const int fc_args_start = 6;
14141416

1415-
// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, [cconv | (cconv, effects)], args..., roots...)
1417+
// Expr(:foreigncall, pointer, rettype, (argtypes...), nreq, gc_safe, [cconv | (cconv, effects)], args..., roots...)
14161418
static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs)
14171419
{
14181420
JL_NARGSV(ccall, 5);
@@ -1424,11 +1426,13 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs)
14241426
assert(jl_is_quotenode(args[5]));
14251427
jl_value_t *jlcc = jl_quotenode_value(args[5]);
14261428
jl_sym_t *cc_sym = NULL;
1429+
bool gc_safe = false;
14271430
if (jl_is_symbol(jlcc)) {
14281431
cc_sym = (jl_sym_t*)jlcc;
14291432
}
14301433
else if (jl_is_tuple(jlcc)) {
14311434
cc_sym = (jl_sym_t*)jl_get_nth_field_noalloc(jlcc, 0);
1435+
gc_safe = jl_unbox_bool(jl_get_nth_field_checked(jlcc, 2));
14321436
}
14331437
assert(jl_is_symbol(cc_sym));
14341438
native_sym_arg_t symarg = {};
@@ -1547,7 +1551,7 @@ static jl_cgval_t emit_ccall(jl_codectx_t &ctx, jl_value_t **args, size_t nargs)
15471551
}
15481552
if (rt != args[2] && rt != (jl_value_t*)jl_any_type)
15491553
jl_temporary_root(ctx, rt);
1550-
function_sig_t sig("ccall", lrt, rt, retboxed,
1554+
function_sig_t sig("ccall", lrt, rt, retboxed, gc_safe,
15511555
(jl_svec_t*)at, unionall, nreqargs,
15521556
cc, llvmcall, &ctx.emission_context);
15531557
for (size_t i = 0; i < nccallargs; i++) {
@@ -2158,11 +2162,16 @@ jl_cgval_t function_sig_t::emit_a_ccall(
21582162
}
21592163
}
21602164

2161-
OperandBundleDef OpBundle("jl_roots", gc_uses);
2165+
// Potentially we could drop `jl_roots(gc_uses)` in the presence of `gc-transition(gc_uses)`
2166+
SmallVector<OperandBundleDef, 2> bundles;
2167+
if (!gc_uses.empty())
2168+
bundles.push_back(OperandBundleDef("jl_roots", gc_uses));
2169+
if (gc_safe)
2170+
bundles.push_back(OperandBundleDef("gc-transition", ArrayRef<Value*> {}));
21622171
// the actual call
21632172
CallInst *ret = ctx.builder.CreateCall(functype, llvmf,
21642173
argvals,
2165-
ArrayRef<OperandBundleDef>(&OpBundle, gc_uses.empty() ? 0 : 1));
2174+
bundles);
21662175
((CallInst*)ret)->setAttributes(attributes);
21672176

21682177
if (cc != CallingConv::C)

src/codegen.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8041,7 +8041,7 @@ static jl_cgval_t emit_cfunction(jl_codectx_t &ctx, jl_value_t *output_type, con
80418041
if (rt != declrt && rt != (jl_value_t*)jl_any_type)
80428042
jl_temporary_root(ctx, rt);
80438043

8044-
function_sig_t sig("cfunction", lrt, rt, retboxed, argt, unionall_env, false, CallingConv::C, false, &ctx.emission_context);
8044+
function_sig_t sig("cfunction", lrt, rt, retboxed, false, argt, unionall_env, false, CallingConv::C, false, &ctx.emission_context);
80458045
assert(sig.fargt.size() + sig.sret == sig.fargt_sig.size());
80468046
if (!sig.err_msg.empty()) {
80478047
emit_error(ctx, sig.err_msg);
@@ -8181,7 +8181,7 @@ const char *jl_generate_ccallable(Module *llvmmod, void *sysimg_handle, jl_value
81818181
}
81828182
jl_value_t *err;
81838183
{ // scope block for sig
8184-
function_sig_t sig("cfunction", lcrt, crt, toboxed,
8184+
function_sig_t sig("cfunction", lcrt, crt, toboxed, false,
81858185
argtypes, NULL, false, CallingConv::C, false, &params);
81868186
if (sig.err_msg.empty()) {
81878187
if (sysimg_handle) {

src/llvm-codegen-shared.h

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,21 +244,17 @@ static inline llvm::Value *emit_gc_state_set(llvm::IRBuilder<> &builder, llvm::T
244244
unsigned offset = offsetof(jl_tls_states_t, gc_state);
245245
Value *gc_state = builder.CreateConstInBoundsGEP1_32(T_int8, ptls, offset, "gc_state");
246246
if (old_state == nullptr) {
247-
old_state = builder.CreateLoad(T_int8, gc_state);
247+
old_state = builder.CreateLoad(T_int8, gc_state, "old_state");
248248
cast<LoadInst>(old_state)->setOrdering(AtomicOrdering::Monotonic);
249249
}
250250
builder.CreateAlignedStore(state, gc_state, Align(sizeof(void*)))->setOrdering(AtomicOrdering::Release);
251251
if (auto *C = dyn_cast<ConstantInt>(old_state))
252-
if (C->isZero())
253-
return old_state;
254-
if (auto *C = dyn_cast<ConstantInt>(state))
255-
if (!C->isZero())
256-
return old_state;
252+
if (auto *C2 = dyn_cast<ConstantInt>(state))
253+
if (C->getZExtValue() == C2->getZExtValue())
254+
return old_state;
257255
BasicBlock *passBB = BasicBlock::Create(builder.getContext(), "safepoint", builder.GetInsertBlock()->getParent());
258256
BasicBlock *exitBB = BasicBlock::Create(builder.getContext(), "after_safepoint", builder.GetInsertBlock()->getParent());
259-
Constant *zero8 = ConstantInt::get(T_int8, 0);
260-
builder.CreateCondBr(builder.CreateOr(builder.CreateICmpEQ(old_state, zero8), // if (!old_state || !state)
261-
builder.CreateICmpEQ(state, zero8)),
257+
builder.CreateCondBr(builder.CreateICmpEQ(old_state, state, "is_new_state"), // Safepoint whenever we change the GC state
262258
passBB, exitBB);
263259
builder.SetInsertPoint(passBB);
264260
MDNode *tbaa = get_tbaa_const(builder.getContext());

0 commit comments

Comments
 (0)