mirror of
https://github.com/ruby/ruby.git
synced 2026-01-26 12:14:51 +00:00
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:
parent
dc58d58a72
commit
07b2356a6a
Notes:
git
2025-12-11 22:26:27 +00:00
9
eval.c
9
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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
224
thread_sync.c
224
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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user