forked from crystal-lang/crystal
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Based on the PR by @BlobCodes: crystal-lang#15216 The performance improvement is two-fold: 1. the usage of a i8 instead of an i1 boolean to have 3 states instead of 2, which permits to quickly detect recursive calls without an array; 2. inline tricks to optimize the fast and slow paths. Unlike the PR: 1. Doesn't use atomics: it already uses a mutex that guarantees acquire release memory ordering semantics, and __crystal_once_init is only ever called in the main thread before any other thread is started. 2. Removes the need for a state maintained by the compiler, yet keeps forward and backward compatibility (both signatures are supported).
- Loading branch information
1 parent
576a14c
commit 43421ce
Showing
4 changed files
with
145 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,119 @@ | ||
# 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) | ||
# 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.15.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 | ||
enum OnceState : Int8 | ||
Processing = -1 | ||
Uninitialized = 0 | ||
Initialized = 1 | ||
end | ||
|
||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@@once_mutex = uninitialized Mutex | ||
class_property once_mutex : Mutex | ||
{% end %} | ||
|
||
# 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) %} | ||
Crystal.once_mutex.synchronize { once_exec(flag, initializer) } | ||
{% else %} | ||
once_exec(flag, initializer) | ||
{% 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 | ||
@rec << flag | ||
end | ||
end | ||
|
||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = true | ||
# :nodoc: | ||
fun __crystal_once_init : Nil | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
Crystal.once_mutex = Mutex.new(:reentrant) | ||
{% end %} | ||
end | ||
|
||
@rec.pop | ||
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 : Int8*, initializer : Void*) : Nil | ||
flag = flag.as(Crystal::OnceState*) | ||
Crystal.once(flag, initializer) 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. | ||
|
||
# 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: | ||
class Crystal::OnceState | ||
@rec = [] of Bool* | ||
|
||
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) | ||
|
||
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: | ||
fun __crystal_once(state : Void*, flag : Bool*, initializer : Void*) | ||
state.as(Crystal::OnceState).once(flag, initializer) | ||
end | ||
{% end %} |