From 07b2356a6ad314b9a7b2bb9fc0527b440f004faa Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 10 Dec 2025 11:44:27 +0100 Subject: [PATCH] Mutex: avoid repeated calls to `GET_EC` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That call is surprisingly expensive, so trying doing it once in `#synchronize` and then passing the EC to lock and unlock saves quite a few cycles. Before: ``` ruby 4.0.0dev (2025-12-10T09:30:18Z master c5608ab4d7) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 1.888M i/100ms Monitor 1.633M i/100ms Calculating ------------------------------------- Mutex 22.610M (± 0.2%) i/s (44.23 ns/i) - 113.258M in 5.009097s Monitor 19.148M (± 0.3%) i/s (52.22 ns/i) - 96.366M in 5.032755s ``` After: ``` ruby 4.0.0dev (2025-12-10T10:40:07Z speedup-mutex 1c901cd4f8) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.095M i/100ms Monitor 1.578M i/100ms Calculating ------------------------------------- Mutex 24.456M (± 0.4%) i/s (40.89 ns/i) - 123.584M in 5.053418s Monitor 19.176M (± 0.1%) i/s (52.15 ns/i) - 96.243M in 5.018977s ``` Bench: ``` require 'bundler/inline' gemfile do gem "benchmark-ips" end mutex = Mutex.new require "monitor" monitor = Monitor.new Benchmark.ips do |x| x.report("Mutex") { mutex.synchronize { } } x.report("Monitor") { monitor.synchronize { } } end ``` --- eval.c | 9 +- internal/eval.h | 2 + thread_sync.c | 224 +++++++++++++++++++++++------------------------- thread_sync.rb | 83 ++++++++++++++++++ 4 files changed, 198 insertions(+), 120 deletions(-) diff --git a/eval.c b/eval.c index ee5bc43f9c..0c80872bee 100644 --- a/eval.c +++ b/eval.c @@ -1133,12 +1133,11 @@ rb_protect(VALUE (* proc) (VALUE), VALUE data, int *pstate) } VALUE -rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) +rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) { enum ruby_tag_type state; volatile VALUE result = Qnil; VALUE errinfo; - rb_execution_context_t * volatile ec = GET_EC(); EC_PUSH_TAG(ec); if ((state = EC_EXEC_TAG()) == TAG_NONE) { result = (*b_proc) (data1); @@ -1155,6 +1154,12 @@ rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE dat return result; } +VALUE +rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2) +{ + return rb_ec_ensure(GET_EC(), b_proc, data1, e_proc, data2); +} + static ID frame_func_id(const rb_control_frame_t *cfp) { diff --git a/internal/eval.h b/internal/eval.h index 4c1c045b4e..17ade0a7f1 100644 --- a/internal/eval.h +++ b/internal/eval.h @@ -11,6 +11,7 @@ * header (related to this file, but not the same role). */ #include "ruby/ruby.h" /* for ID */ +#include "vm_core.h" /* for ID */ #define id_signo ruby_static_id_signo #define id_status ruby_static_id_status @@ -30,6 +31,7 @@ VALUE rb_exception_setup(int argc, VALUE *argv); void rb_refinement_setup(struct rb_refinements_data *data, VALUE module, VALUE klass); void rb_vm_using_module(VALUE module); VALUE rb_top_main_class(const char *method); +VALUE rb_ec_ensure(rb_execution_context_t *ec, VALUE (*b_proc)(VALUE), VALUE data1, VALUE (*e_proc)(VALUE), VALUE data2); /* eval_error.c */ VALUE rb_get_backtrace(VALUE info); diff --git a/thread_sync.c b/thread_sync.c index 9fb1639e9b..9942d08e0a 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -123,7 +123,7 @@ rb_mutex_num_waiting(rb_mutex_t *mutex) rb_thread_t* rb_fiber_threadptr(const rb_fiber_t *fiber); static bool -locked_p(rb_mutex_t *mutex) +mutex_locked_p(rb_mutex_t *mutex) { return mutex->fiber_serial != 0; } @@ -132,7 +132,7 @@ static void mutex_free(void *ptr) { rb_mutex_t *mutex = ptr; - if (locked_p(mutex)) { + if (mutex_locked_p(mutex)) { const char *err = rb_mutex_unlock_th(mutex, rb_thread_ptr(mutex->thread), NULL); if (err) rb_bug("%s", err); } @@ -179,36 +179,18 @@ mutex_alloc(VALUE klass) return obj; } -/* - * call-seq: - * Thread::Mutex.new -> mutex - * - * Creates a new Mutex - */ -static VALUE -mutex_initialize(VALUE self) -{ - return self; -} - VALUE rb_mutex_new(void) { return mutex_alloc(rb_cMutex); } -/* - * call-seq: - * mutex.locked? -> true or false - * - * Returns +true+ if this lock is currently held by some thread. - */ VALUE rb_mutex_locked_p(VALUE self) { rb_mutex_t *mutex = mutex_ptr(self); - return RBOOL(locked_p(mutex)); + return RBOOL(mutex_locked_p(mutex)); } static void @@ -267,17 +249,16 @@ mutex_trylock(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) } } -/* - * call-seq: - * mutex.try_lock -> true or false - * - * Attempts to obtain the lock and returns immediately. Returns +true+ if the - * lock was granted. - */ +static VALUE +rb_mut_trylock(rb_execution_context_t *ec, VALUE self) +{ + return RBOOL(mutex_trylock(mutex_ptr(self), ec->thread_ptr, ec->fiber_ptr)); +} + VALUE rb_mutex_trylock(VALUE self) { - return RBOOL(mutex_trylock(mutex_ptr(self), GET_THREAD(), GET_EC()->fiber_ptr)); + return rb_mut_trylock(GET_EC(), self); } static VALUE @@ -303,13 +284,28 @@ delete_from_waitq(VALUE value) static inline rb_atomic_t threadptr_get_interrupts(rb_thread_t *th); -static VALUE -do_mutex_lock(VALUE self, int interruptible_p) +struct mutex_args { + VALUE self; + rb_mutex_t *mutex; + rb_execution_context_t *ec; +}; + +static inline void +mutex_args_init(struct mutex_args *args, VALUE mutex) { - rb_execution_context_t *ec = GET_EC(); + args->self = mutex; + args->mutex = mutex_ptr(mutex); + args->ec = GET_EC(); +} + +static VALUE +do_mutex_lock(struct mutex_args *args, int interruptible_p) +{ + VALUE self = args->self; + rb_execution_context_t *ec = args->ec; rb_thread_t *th = ec->thread_ptr; rb_fiber_t *fiber = ec->fiber_ptr; - rb_mutex_t *mutex = mutex_ptr(self); + rb_mutex_t *mutex = args->mutex; rb_atomic_t saved_ints = 0; /* When running trap handler */ @@ -432,35 +428,40 @@ do_mutex_lock(VALUE self, int interruptible_p) static VALUE mutex_lock_uninterruptible(VALUE self) { - return do_mutex_lock(self, 0); + struct mutex_args args; + mutex_args_init(&args, self); + return do_mutex_lock(&args, 0); +} + +static VALUE +rb_mut_lock(rb_execution_context_t *ec, VALUE self) +{ + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + return do_mutex_lock(&args, 1); } -/* - * call-seq: - * mutex.lock -> self - * - * Attempts to grab the lock and waits if it isn't available. - * Raises +ThreadError+ if +mutex+ was locked by the current thread. - */ VALUE rb_mutex_lock(VALUE self) { - return do_mutex_lock(self, 1); + struct mutex_args args; + mutex_args_init(&args, self); + return do_mutex_lock(&args, 1); +} + +static VALUE +rb_mut_owned_p(rb_execution_context_t *ec, VALUE self) +{ + return mutex_owned_p(ec->fiber_ptr, mutex_ptr(self)); } -/* - * call-seq: - * mutex.owned? -> true or false - * - * Returns +true+ if this lock is currently held by current thread. - */ VALUE rb_mutex_owned_p(VALUE self) { - rb_fiber_t *fiber = GET_EC()->fiber_ptr; - rb_mutex_t *mutex = mutex_ptr(self); - - return mutex_owned_p(fiber, mutex); + return rb_mut_owned_p(GET_EC(), self); } static const char * @@ -508,6 +509,24 @@ rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) return NULL; } +static void +do_mutex_unlock(struct mutex_args *args) +{ + const char *err; + rb_mutex_t *mutex = args->mutex; + rb_thread_t *th = rb_ec_thread_ptr(args->ec); + + err = rb_mutex_unlock_th(mutex, th, args->ec->fiber_ptr); + if (err) rb_raise(rb_eThreadError, "%s", err); +} + +static VALUE +do_mutex_unlock_safe(VALUE args) +{ + do_mutex_unlock((struct mutex_args *)args); + return Qnil; +} + /* * call-seq: * mutex.unlock -> self @@ -518,13 +537,21 @@ rb_mutex_unlock_th(rb_mutex_t *mutex, rb_thread_t *th, rb_fiber_t *fiber) VALUE rb_mutex_unlock(VALUE self) { - const char *err; - rb_mutex_t *mutex = mutex_ptr(self); - rb_thread_t *th = GET_THREAD(); - - err = rb_mutex_unlock_th(mutex, th, GET_EC()->fiber_ptr); - if (err) rb_raise(rb_eThreadError, "%s", err); + struct mutex_args args; + mutex_args_init(&args, self); + do_mutex_unlock(&args); + return self; +} +static VALUE +rb_mut_unlock(rb_execution_context_t *ec, VALUE self) +{ + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + do_mutex_unlock(&args); return self; } @@ -593,17 +620,15 @@ mutex_sleep_begin(VALUE _arguments) return woken; } -VALUE -rb_mutex_sleep(VALUE self, VALUE timeout) +static VALUE +rb_mut_sleep(rb_execution_context_t *ec, VALUE self, VALUE timeout) { - rb_execution_context_t *ec = GET_EC(); - if (!NIL_P(timeout)) { // Validate the argument: rb_time_interval(timeout); } - rb_mutex_unlock(self); + rb_mut_unlock(ec, self); time_t beg = time(0); struct rb_mutex_sleep_arguments arguments = { @@ -611,7 +636,7 @@ rb_mutex_sleep(VALUE self, VALUE timeout) .timeout = timeout, }; - VALUE woken = rb_ensure(mutex_sleep_begin, (VALUE)&arguments, mutex_lock_uninterruptible, self); + VALUE woken = rb_ec_ensure(ec, mutex_sleep_begin, (VALUE)&arguments, mutex_lock_uninterruptible, self); RUBY_VM_CHECK_INTS_BLOCKING(ec); if (!woken) return Qnil; @@ -619,61 +644,32 @@ rb_mutex_sleep(VALUE self, VALUE timeout) return TIMET2NUM(end); } -/* - * call-seq: - * mutex.sleep(timeout = nil) -> number or nil - * - * Releases the lock and sleeps +timeout+ seconds if it is given and - * non-nil or forever. Raises +ThreadError+ if +mutex+ wasn't locked by - * the current thread. - * - * When the thread is next woken up, it will attempt to reacquire - * the lock. - * - * Note that this method can wakeup without explicit Thread#wakeup call. - * For example, receiving signal and so on. - * - * Returns the slept time in seconds if woken up, or +nil+ if timed out. - */ -static VALUE -mutex_sleep(int argc, VALUE *argv, VALUE self) +VALUE +rb_mutex_sleep(VALUE self, VALUE timeout) { - VALUE timeout; - - timeout = rb_check_arity(argc, 0, 1) ? argv[0] : Qnil; - return rb_mutex_sleep(self, timeout); + return rb_mut_sleep(GET_EC(), self, timeout); } -/* - * call-seq: - * mutex.synchronize { ... } -> result of the block - * - * Obtains a lock, runs the block, and releases the lock when the block - * completes. See the example under Thread::Mutex. - */ VALUE -rb_mutex_synchronize(VALUE mutex, VALUE (*func)(VALUE arg), VALUE arg) +rb_mutex_synchronize(VALUE self, VALUE (*func)(VALUE arg), VALUE arg) { - rb_mutex_lock(mutex); - return rb_ensure(func, arg, rb_mutex_unlock, mutex); + struct mutex_args args; + mutex_args_init(&args, self); + do_mutex_lock(&args, 1); + return rb_ec_ensure(args.ec, func, arg, do_mutex_unlock_safe, (VALUE)&args); } -/* - * call-seq: - * mutex.synchronize { ... } -> result of the block - * - * Obtains a lock, runs the block, and releases the lock when the block - * completes. See the example under Thread::Mutex. - */ -static VALUE -rb_mutex_synchronize_m(VALUE self) +VALUE +rb_mut_synchronize(rb_execution_context_t *ec, VALUE self) { - if (!rb_block_given_p()) { - rb_raise(rb_eThreadError, "must be called with a block"); - } - - return rb_mutex_synchronize(self, rb_yield, Qundef); + struct mutex_args args = { + .self = self, + .mutex = mutex_ptr(self), + .ec = ec, + }; + do_mutex_lock(&args, 1); + return rb_ec_ensure(args.ec, rb_yield, Qundef, do_mutex_unlock_safe, (VALUE)&args); } void @@ -1688,14 +1684,6 @@ Init_thread_sync(void) /* Mutex */ DEFINE_CLASS(Mutex, Object); rb_define_alloc_func(rb_cMutex, mutex_alloc); - rb_define_method(rb_cMutex, "initialize", mutex_initialize, 0); - rb_define_method(rb_cMutex, "locked?", rb_mutex_locked_p, 0); - rb_define_method(rb_cMutex, "try_lock", rb_mutex_trylock, 0); - rb_define_method(rb_cMutex, "lock", rb_mutex_lock, 0); - rb_define_method(rb_cMutex, "unlock", rb_mutex_unlock, 0); - rb_define_method(rb_cMutex, "sleep", mutex_sleep, -1); - rb_define_method(rb_cMutex, "synchronize", rb_mutex_synchronize_m, 0); - rb_define_method(rb_cMutex, "owned?", rb_mutex_owned_p, 0); /* Queue */ DEFINE_CLASS(Queue, Object); diff --git a/thread_sync.rb b/thread_sync.rb index f8fa69900b..28c70b1e9c 100644 --- a/thread_sync.rb +++ b/thread_sync.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Thread class Queue # call-seq: @@ -65,4 +67,85 @@ class Thread alias_method :enq, :push alias_method :<<, :push end + + class Mutex + # call-seq: + # Thread::Mutex.new -> mutex + # + # Creates a new Mutex + def initialize + end + + # call-seq: + # mutex.locked? -> true or false + # + # Returns +true+ if this lock is currently held by some thread. + def locked? + Primitive.cexpr! %q{ RBOOL(mutex_locked_p(mutex_ptr(self))) } + end + + # call-seq: + # mutex.owned? -> true or false + # + # Returns +true+ if this lock is currently held by current thread. + def owned? + Primitive.rb_mut_owned_p + end + + # call-seq: + # mutex.lock -> self + # + # Attempts to grab the lock and waits if it isn't available. + # Raises +ThreadError+ if +mutex+ was locked by the current thread. + def lock + Primitive.rb_mut_lock + end + + # call-seq: + # mutex.try_lock -> true or false + # + # Attempts to obtain the lock and returns immediately. Returns +true+ if the + # lock was granted. + def try_lock + Primitive.rb_mut_trylock + end + + # call-seq: + # mutex.lock -> self + # + # Attempts to grab the lock and waits if it isn't available. + # Raises +ThreadError+ if +mutex+ was locked by the current thread. + def unlock + Primitive.rb_mut_unlock + end + + # call-seq: + # mutex.synchronize { ... } -> result of the block + # + # Obtains a lock, runs the block, and releases the lock when the block + # completes. See the example under Thread::Mutex. + def synchronize + raise ThreadError, "must be called with a block" unless defined?(yield) + + Primitive.rb_mut_synchronize + end + + # call-seq: + # mutex.sleep(timeout = nil) -> number or nil + # + # Releases the lock and sleeps +timeout+ seconds if it is given and + # non-nil or forever. Raises +ThreadError+ if +mutex+ wasn't locked by + # the current thread. + # + # When the thread is next woken up, it will attempt to reacquire + # the lock. + # + # Note that this method can wakeup without explicit Thread#wakeup call. + # For example, receiving signal and so on. + # + # Returns the slept time in seconds if woken up, or +nil+ if timed out. + def sleep(timeout = nil) + Primitive.rb_mut_sleep(timeout) + end + end end