8.4. Safepoint protocol
For the GC to perform temporary STW (Stop-The-World) work, every mutator thread must have reached a safe point. This document defines the ABI protocol for that synchronization.
8.4.1. yield flag check: fast path
Mutators periodically check the TLS slot rok_tls_yield_flag (u8). The check proceeds as follows.
poll:
flag = TLS[yield_flag]
if flag != 0:
call rok_runtime_yield() // slow path
// proceed normally
In LLVM IR:
%t = call ptr @llvm.threadlocal.address(ptr @rok_tls)
%flag_p = getelementptr i8, ptr %t, i64 88 ; yield_flag at 10p+8, p = 8
%flag = load i8, ptr %flag_p
%check = icmp ne i8 %flag, 0
br i1 %check, label %slow, label %cont
slow:
call void @rok_runtime_yield()
br label %cont
cont:
; ... continue
This is a two-instruction fast path plus one branch, and the branch almost always evaluates to false.
8.4.2. Emission sites
The compiler emits safepoint polls at the following sites.
| Site | Reason |
|---|---|
| Function entry (or before the first safepoint of each path) | Can cooperate with the GC right after a call |
| Loop back-edge | Prevents an infinite loop from blocking the GC |
| Right after a function call (optional) | Cooperate immediately if the call was long |
The detailed emission policy is up to the compiler; this specification guarantees only the following minimum frequency.
- Every loop back-edge has a poll, except a loop with a compile-time constant trip count of at most 64 whose body reaches no safepoint; every cycle of calls contains a poll.
- A function that can reach no safepoint (no loop but such bounded loops, no allocation, indirect call, lazy
static, or call to a function that may reach one) needs none.
Together these bound the time from a yield request to the next safepoint.
8.4.3. rok_runtime_yield: slow path
The slow path signature is:
extern "C" {
fn rok_runtime_yield();
}
A mutator calls this function to cooperate with the GC. Its effects are:
- Sets
mutator_statein the mutator's Mutator TLS slot layout toGC_COOPERATING. - Exposes the root information the GC needs, such as the shadow stack walk.
- Waits until the GC finishes.
- When the GC finishes, sets
mutator_stateback toRUNNING.
yield_flag is cleared automatically while inside rok_runtime_yield, or is cleared by the GC.
8.4.4. Setting / clearing the yield flag
When the GC needs an STW pause:
gc_initiate_stw():
for each mutator thread:
atomic store TLS[yield_flag] = 1
// wait until every mutator yields
for each mutator thread:
wait until mutator_state == GC_COOPERATING
When the GC finishes STW:
gc_finish_stw():
for each mutator thread:
atomic store TLS[yield_flag] = 0
notify all (resume)
8.4.5. Fast path cost
The cost borne by a mutator is a single load of the yield flag and one branch.
In almost all cases the flag is 0, so the branch is not taken, and the branch predictor of modern CPUs handles it at nearly zero cost.
For that cost, every thread gets a deterministic cooperative synchronization mechanism.
8.4.6. STW phases
STW collection (minor, full with evacuation, rok_gc_collect) is a single pause. The thread that started the collection stops all other mutators (GC_COOPERATING, or PARKED if inside a blocking runtime call), finishes the collection and resumes them.
A full collection with concurrent mark has two pauses, an initial and a final one (7.5.2). In between, a single marker thread runs alongside the mutators.
8.4.7. Safepoint invariant
When a safepoint is reached, every live GC object variable of the mutator must be registered in the shadow stack.
The compiler guarantees this by linking the shadow stack frame from Barriers and root tracking before the first safepoint on each path through the function: at entry, or later on a path that reaches one. A path that reaches no safepoint need not link it, and a frame is unlinked on return only where it was linked. Likewise, a function entered only by direct rok calls may move its entry poll to just after the link: every cycle of calls still contains a poll, since the call that closes the cycle is a safepoint. A function called from C or through a function value polls at entry. A local holding GC references at a safepoint is registered in the frame, and the registered word is kept current at every write. A local only back-edge polls see may instead be registered by the poll's slow path just for the runtime call (7.4.1 rule 4), so a loop whose only safepoints are its polls links no frame and writes no root word while it runs. The compiler may also keep a private copy (in a register), since a collection never rewrites a root word: objects reached directly from roots are not moved (7.4.1).
8.4.8. Loop back-edge poll
If a long loop does not cooperate with the GC, STW waits forever. The compiler therefore emits a poll on every loop back-edge (8.4.2's bounded loops aside).
loop:
body
poll // back-edge poll
branch loop
The cost of this poll is again two instructions and one branch, negligible compared to the loop body.
The poll stays on the back edge. Its slow path (the rok_runtime_yield call) may branch to a re-entry block placed before the loop header instead of returning into the edge.
The compiler detects every natural loop and emits a poll for each but the bounded loops of 8.4.2.
8.4.9. Inline functions and polls
When a small function is inlined, its entry poll may disappear with it. The inlined result is absorbed into the caller's body, however, so the caller's entry poll suffices.
When large-scale inlining happens, an LLVM pass adds extra polls automatically (an implementation decision).
8.4.10. Native calls
A thread inside C or a blocking runtime call cannot reach a safepoint, so it counts as PARKED: the GC treats it as already cooperating.
- The compiler brackets every
extern "C"call:PARKEDfor the duration of the call,RUNNINGafter it, then a yield-flag check. The caller's argument roots stay in its shadow-stack frame. The fast path is inline;rok_enter_native/rok_leave_nativeare the slow paths (8.5.2). - Every
extern "C" fndefinition starts withrok_native_reenterand ends withrok_native_resume, so a C callback runs as a mutator. - The runtime brackets its own blocking calls (I/O wait,
rok_sync_wait, join, sleep) the same way, and its long computations that touch no GC object but buffers the caller's frame keeps alive (theintmultiplication, division and gcd kernels). Theirextern "rok"declarations are not#[no_safepoint](5.9.2), so a call to them is a safepoint and the buffers are rooted. - A call through an
extern "C"declaration of arok_*symbol (8.5, the runtime's reserved prefix) is not bracketed: the runtime function runs as a mutator.
call_extern_c:
mutator_state = PARKED // seq_cst
c_function()
mutator_state = RUNNING // seq_cst
if yield_flag:
rok_leave_native() // wait out a running collection
8.4.11. Direct calls from user code
User code may also (rarely) call rok_runtime_yield() directly.
extern "C" {
fn rok_runtime_yield();
}
fn long_loop() {
for i in 0..1_000_000 {
...
if i % 1000 == 0 { rok_runtime_yield(); }
}
}
rok_runtime_yield is a rok_* symbol, so the call is not bracketed (8.4.10) and the thread yields as a running mutator. The compiler already emits polls on loop back-edges, so users almost never need to call it directly. This pattern is used only in special situations such as inside inline assembly.
8.4.12. Stability
The ABI is the slot at offset 10p+8 of rok_tls (rok_tls_yield_flag) and its type (u8) (8.3.1).
rok_runtime_yield follows a simple calling convention with no arguments and no return value.