Project

General

Profile

Feature #22067

Updated by jhawthorn (John Hawthorn) 4 months ago

CRuby `TypedData` types are used internally in the VM and in C extensions. Currently extensions and currently any `dfree` functions, which usually `free` functions for these types are declared `RUBY_TYPED_FREE_IMMEDIATELY`, are run with the VM lock held and a as well as the VM barrier stopping all ractors and threads. (or, in the case of MMTk, are not freed in parallel). In short, no other Ruby threads or GC threads can run while these `free` functions are called. To be specific In order to allow one or more worker GC threads to free these `TypedData` objects, we need a way to specify that a `TypedData` type can be run any time an allocation (newobj freed in parallel alongside the Ruby GC thread or xmalloc) happens (or `CHECK_INTS`), which Ruby code that is when GC may trigger. The function will be called synchronously being run by another thread. Otherwise, we cannot free these types in the workers and finish before must rely on the allocation returns. 

 Though much more relaxed than what's required without `RUBY_TYPED_FREE_IMMEDIATELY`, this expectation limits what Ruby GC implementations thread to do so. This is because it can do. 

 To allow more flexibility in GC implementations, we want be unsafe to let worker GC threads free call these TypedData objects concurrently with other Ruby code or in parallel with other GC threads. `free` functions, depending on how they're implemented. 

 Most `TypedData` `dfree` functions are simply calls safe to `xfree` and are trivially thread safe. free in parallel. The exceptions are `dfree` those `free` functions that read or modify global state without locking. 


 

 ### Examples 

 **Safe** 

 ```c 
 static void 
 safe_data_free(void *ptr) 
 { 
     // thread safe 
     safe_data *data = ptr; 
     xfree(data->buffer); 
     xfree(data); 
 } 
 ``` 

 **Unsafe due to shared state** 

 ```c 
 static void 
 example_data_free(void *ptr) 
 { 
     st_delete(live_example_datas, (st_data_t*)&ptr, NULL); // Not thread-safe! 
 } 
 ``` 

 There are two reasons this would not be thread-safe to free: 
 * Unsafe for parallel free - If 2 of these `TypedData` objects could be are freed at the same time, corrupting this could corrupt the `st_table` 
 * Unsafe for concurrent free - `live_example_datas` would `st_table`. A lock must be inserted into and iterated by held when manipulating this table. 

 Because Ruby code 

 For types like this which mutate can also run alongside a piece of shared state, they should either not sweep worker, the lock must also be declared as thread-safe held when adding to or some form of locking should be used when manipulating the iterating this table. 

 Most types are trivially thread-safe, but there are enough exceptions in ```c 
 rb_nativethread_lock_t example_data_lock; 

 static void 
 example_data_free(void *ptr) 
 { 
     rb_native_mutex_lock(&example_data_lock); 
     st_delete(live_example_datas, (st_data_t*)&ptr, NULL); 
     rb_native_mutex_unlock(&example_data_lock); 
 } 

 static void 
 live_examples_add(void *ptr) 
 { 
     rb_native_mutex_lock(&example_data_lock); 
     st_insert(live_example_datas, (st_data_t)ptr, (st_data_t)ptr); 
     rb_native_mutex_unlock(&example_data_lock); 
 } 

 static void 
 Init_example(void) 
 { 
     rb_native_mutex_initialize(&example_data_lock); 
 } 
 ``` 

 If a CRuby itself that it needs developer or user wants to be opt-in, make their code compaction-safe, they don’t need to worry about parallel sweep workers because the same way RUBY_TYPED_FREE_IMMEDIATELY is. workers don't run during compaction. 

 ### Proposal 

 We I propose adding a new flag to `TypedData`, `RUBY_TYPED_THREAD_SAFE_FREE` `TypedData` that allows both CRuby itself developers and extension authors to opt-in to declaring allow their `TypedData` type to be freed in parallel. It could be called something like `RUBY_TYPED_PARALLEL_FREE_SAFE`. In our branch where we are developing parallel sweeping, most `TypedData` internal to the VM are given this flag. I believe C extension authors would opt in as thread safe. well if they see that parallel sweeping gives good performance benefits to Ruby applications. 

 Proposed documentation We would need to `doc/extension.rdoc` 

 ``` 
 RUBY_TYPED_THREAD_SAFE_FREE :: 

   This flag declares document what is safe and what is not safe inside these `free` functions for types that are marked with this bit. If the dfree() user needs to use a native mutex to protect their `TypedData` from being corrupted when freed in parallel, they must do so. However, if they lock this native mutex in non-free function code paths as well, they may not allocate objects or use `ruby_xmalloc` while this mutex is thread-safe. 

   With this flag set held. These are some examples of trickiness when it comes to concurrency in the garbage collector MAY invoke dfree() 
     * On any thread, whether a Ruby Thread or VM. I believe having a native thread 
     * In section in the extension documentation about these types of free functions (or elsewhere such as the Concurrency Guide) would give CRuby developers and extension authors more confidence in adding this bit to their types. 

 ### A possible future: parallel marking? 

 If in the future CRuby gets parallel marking, we believe we probably would need another bit for `TypedData` so we can register them as parallel-mark safe. If that’s the case, it’s unfortunate that authors that want this feature would need to update their extension again with calls a new bit if this bit were to be made public. One could argue that we should have a single bit that indicates safety for both parallel freeing and parallel marking. However, we believe the same or other dfree specifics of what could and couldn’t be executed inside these `free` and `mark` functions 
     * Concurrently with other Ruby code 

   This should NOT would be set on dfree() functions which access shared state in too hard to work out today for a 
   thread-unsafe way. A dfree() function which uses combined bit without locking us into a specific parallel marking implementation. 

 ### Alternative 

 We could not expose the parallel-free-safe bit to extension authors and only xfree() on free internal `TypedData` objects in parallel. However, this does slow the struct 
   and memory it exclusively owns current implementation of parallel sweeping down because even if one of these objects is thread-safe. 

   This flag implies RUBY_TYPED_FREE_IMMEDIATELY, since on a thread-safe dfree() 
   is heap page, the Ruby GC thread needs to further post-process the page after the sweep thread sweeps it. It would also safe limit further optimizations to invoke immediately. 
 ``` parallel sweeping. 

 Per ### Details of the bit 

 Worker threads could only free `TypedData` objects that doc, have this is purely a more strict dfree constraint from new bit set alongside the TypedData implementer's perspective, and more flexible from `RUBY_TYPED_FREE_IMMEDIATELY` bit. Otherwise, the Ruby GC author's perspective. We can treat anything with this flag set thread must free them. Also, if the same type has `0` or `RUBY_TYPED_DEFAULT_FREE`/`RUBY_DEFAULT_FREE` as `RUBY_TYPED_FREE_IMMEDIATELY` (or the same as no flag set, they are purely incremental free function, it can be freed in how "friendly" the dfree function is). parallel. 

 Many gems can `s/RUBY_TYPED_FREE_IMMEDIATELY/RUBY_TYPED_THREAD_SAFE_FREE/g` with a cursory audit. It's helpful to have one imply ### Conclusion 

 Parallel sweeping is being actively developed and adding this bit increases performance of the other developing implementation of parallel sweeping. It allows `TypedData` types to avoid cluttering the flags (and they happen be freed by worker thread(s), which decreases GC pause times. Since `TypedData` are also exposed to be the exact same length 😅). 

 This forms a series extension authors, we need to whitelist these types for parallel freeing, as not all of bits with increasingly relaxed constraints on when dfree may run: no flag (mutator thread, holding the GVL, at an interrupt check), RUBY_TYPED_FREE_IMMEDIATELY (synchronously during GC, world stopped), these functions are currently safe. By documenting what is and RUBY_TYPED_THREAD_SAFE_FREE (any thread, in parallel, concurrent with Ruby code). isn’t safe, extension authors can write correct `free` functions that are safe to be freed by worker threads. Alternatively, we could have this bit be internal to CRuby.

Back