-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rework initialization of constants & class variables (#15333)
Changes the `flag` that keeps track of initialization status of constants and class variables to have 3 states instead of 2. The third one indicates that the value is currently being initialized and allows detecting recursion. Previously we used an array to keep track of values being currently initialized. This array is now unnecessary. The signature of `__crystal_once` changes: it now takes an Int8 (i8) instead of Bool (i1) and drops the once `state` pointer which isn't needed anymore. So `__crystal_once_init` no longer initializes a state pointer and returns nil. Also introduces a fast path for the (very likely) scenario that the variable is already initialized which doesn't need a mutex. Also introduces an LLVM optimization that instructs LLVM to optimize away repeated calls to `__crystal_once` for the same initializer. Requires a new compiler build to benefit from the improvement. The legacy versions of `__crystal_once` and `__crystal_once_init` are still supported by both the stdlib and the compiler to keep both forward & backward compatibility (1.15 and below can build 1.16+ and 1.16+ can build 1.15 and below). A follow-up could leverage `ReferenceStorage` and `.unsafe_construct` to inline the `Mutex` instead of allocating in the GC heap. Along with #15330 then `__crystal_once_init` could become allocation free, which could prove useful for such a core/low level feature. Co-authored-by: David Keller <[email protected]>
- Loading branch information
1 parent
39aaae5
commit 8d02c8b
Showing
5 changed files
with
188 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,142 @@ | ||
# This file defines the functions `__crystal_once_init` and `__crystal_once` expected | ||
# by the compiler. `__crystal_once` is called each time a constant or class variable | ||
# has to be initialized and is its responsibility to verify the initializer is executed | ||
# only once. `__crystal_once_init` is executed only once at the beginning of the program | ||
# and the result is passed on each call to `__crystal_once`. | ||
|
||
# This implementation uses an array to store the initialization flag pointers for each value | ||
# to find infinite loops and raise an error. In multithread mode a mutex is used to | ||
# avoid race conditions between threads. | ||
|
||
# :nodoc: | ||
class Crystal::OnceState | ||
@rec = [] of Bool* | ||
|
||
def once(flag : Bool*, initializer : Void*) | ||
unless flag.value | ||
if @rec.includes?(flag) | ||
raise "Recursion while initializing class variables and/or constants" | ||
# This file defines two functions expected by the compiler: | ||
# | ||
# - `__crystal_once_init`: executed only once at the beginning of the program | ||
# and, for the legacy implementation, the result is passed on each call to | ||
# `__crystal_once`. | ||
# | ||
# - `__crystal_once`: called each time a constant or class variable has to be | ||
# initialized and is its responsibility to verify the initializer is executed | ||
# only once and to fail on recursion. | ||
|
||
# In multithread mode a mutex is used to avoid race conditions between threads. | ||
# | ||
# On Win32, `Crystal::System::FileDescriptor#@@reader_thread` spawns a new | ||
# thread even without the `preview_mt` flag, and the thread can also reference | ||
# Crystal constants, leading to race conditions, so we always enable the mutex. | ||
|
||
{% if compare_versions(Crystal::VERSION, "1.16.0-dev") >= 0 %} | ||
# This implementation uses an enum over the initialization flag pointer for | ||
# each value to find infinite loops and raise an error. | ||
|
||
module Crystal | ||
# :nodoc: | ||
enum OnceState : Int8 | ||
Processing = -1 | ||
Uninitialized = 0 | ||
Initialized = 1 | ||
end | ||
|
||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@@once_mutex = uninitialized Mutex | ||
|
||
# :nodoc: | ||
def self.once_mutex=(@@once_mutex : Mutex) | ||
end | ||
@rec << flag | ||
{% end %} | ||
|
||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = true | ||
# :nodoc: | ||
# Using @[NoInline] so LLVM optimizes for the hot path (var already | ||
# initialized). | ||
@[NoInline] | ||
def self.once(flag : OnceState*, initializer : Void*) : Nil | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@@once_mutex.synchronize { once_exec(flag, initializer) } | ||
{% else %} | ||
once_exec(flag, initializer) | ||
{% end %} | ||
|
||
@rec.pop | ||
# safety check, and allows to safely call `Intrinsics.unreachable` in | ||
# `__crystal_once` | ||
unless flag.value.initialized? | ||
System.print_error "BUG: failed to initialize constant or class variable\n" | ||
LibC._exit(1) | ||
end | ||
end | ||
|
||
private def self.once_exec(flag : OnceState*, initializer : Void*) : Nil | ||
case flag.value | ||
in .initialized? | ||
return | ||
in .uninitialized? | ||
flag.value = :processing | ||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = :initialized | ||
in .processing? | ||
raise "Recursion while initializing class variables and/or constants" | ||
end | ||
end | ||
end | ||
|
||
# on Win32, `Crystal::System::FileDescriptor#@@reader_thread` spawns a new | ||
# thread even without the `preview_mt` flag, and the thread can also reference | ||
# Crystal constants, leading to race conditions, so we always enable the mutex | ||
# TODO: can this be improved? | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@mutex = Mutex.new(:reentrant) | ||
# :nodoc: | ||
fun __crystal_once_init : Nil | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
Crystal.once_mutex = Mutex.new(:reentrant) | ||
{% end %} | ||
end | ||
|
||
# :nodoc: | ||
# | ||
# Using `@[AlwaysInline]` allows LLVM to optimize const accesses. Since this | ||
# is a `fun` the function will still appear in the symbol table, though it | ||
# will never be called. | ||
@[AlwaysInline] | ||
fun __crystal_once(flag : Crystal::OnceState*, initializer : Void*) : Nil | ||
return if flag.value.initialized? | ||
|
||
Crystal.once(flag, initializer) | ||
|
||
# tell LLVM that it can optimize away repeated `__crystal_once` calls for | ||
# this global (e.g. repeated access to constant in a single funtion); | ||
# this is truly unreachable otherwise `Crystal.once` would have panicked | ||
Intrinsics.unreachable unless flag.value.initialized? | ||
end | ||
{% else %} | ||
# This implementation uses a global array to store the initialization flag | ||
# pointers for each value to find infinite loops and raise an error. | ||
|
||
# :nodoc: | ||
class Crystal::OnceState | ||
@rec = [] of Bool* | ||
|
||
@[NoInline] | ||
def once(flag : Bool*, initializer : Void*) | ||
unless flag.value | ||
@mutex.synchronize do | ||
previous_def | ||
if @rec.includes?(flag) | ||
raise "Recursion while initializing class variables and/or constants" | ||
end | ||
@rec << flag | ||
|
||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = true | ||
|
||
@rec.pop | ||
end | ||
end | ||
{% end %} | ||
end | ||
|
||
# :nodoc: | ||
fun __crystal_once_init : Void* | ||
Crystal::OnceState.new.as(Void*) | ||
end | ||
|
||
# :nodoc: | ||
fun __crystal_once(state : Void*, flag : Bool*, initializer : Void*) | ||
state.as(Crystal::OnceState).once(flag, initializer) | ||
end | ||
|
||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@mutex = Mutex.new(:reentrant) | ||
|
||
@[NoInline] | ||
def once(flag : Bool*, initializer : Void*) | ||
unless flag.value | ||
@mutex.synchronize do | ||
previous_def | ||
end | ||
end | ||
end | ||
{% end %} | ||
end | ||
|
||
# :nodoc: | ||
fun __crystal_once_init : Void* | ||
Crystal::OnceState.new.as(Void*) | ||
end | ||
|
||
# :nodoc: | ||
@[AlwaysInline] | ||
fun __crystal_once(state : Void*, flag : Bool*, initializer : Void*) | ||
return if flag.value | ||
state.as(Crystal::OnceState).once(flag, initializer) | ||
Intrinsics.unreachable unless flag.value | ||
end | ||
{% end %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters