Mutex: avoid repeated calls to GET_EC

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
```
This commit is contained in:
Jean Boussier 2025-12-10 11:44:27 +01:00
parent dc58d58a72
commit 07b2356a6a
Notes: git 2025-12-11 22:26:27 +00:00
4 changed files with 198 additions and 120 deletions

9
eval.c
View File

@ -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)
{

View File

@ -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);

View File

@ -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);

View File

@ -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