Koichi Sasada 2026-08-29 18:39:37 +0000 (Sat, 29 Aug 2026)
New Revision: ce15950f56
https://github.com/ruby/ruby/commit/ce15950f56
Log:
thread_sched: keep io fds registered in epoll with EPOLLONESHOT
A blocking read used to cost four epoll_ctl per wait cycle: ADD before
the wait and DEL after the wake, on both ends of an exchange. The DEL
ran on the timer thread, the one serial resource every io readiness
event already passes through.
With EPOLLONESHOT the delivery itself disarms the fd inside the kernel:
the post-wake DEL becomes no syscall at all, and the next wait re-arms
with one MOD on a registration that persists for the fd's lifetime.
A disarm with no delivery (timeout, interrupt) MODs the event mask to
zero instead of deleting. Registration stays level-triggered, so a
woken reader is under no obligation to drain the fd; the generation tag
already guards against events queued under an old arming, and a stale
registration left by close is repaired by the existing ENOENT fallback
on the next arm.
Two round trips of a 64-byte socketpair exchange drop from 12 to 10
syscalls (epoll_ctl 4.00 -> 2.00) and the timer thread falls from 97%
to 82% of a core. On master 22e4a75d67, sp4 (16 cores), medians:
io_intensive (socketpair round trips, 2 Ractors/pair)
8 pairs 218,839 -> 432,810 rt/s (+98%)
64 pairs 276,556 -> 591,171 rt/s (+114%)
punicorn (one Ractor per connection, wrk from a second box)
keepalive c=64 280,678 -> 309,530 rps (+10%)
4x memcached/req 59,683 -> 67,082 rps (+12%)
SSE probe beside 512 held streams 193,769 -> 205,602 rps (+6%)
Workloads that wait on the timer wheel (sleep), the GVL (cpu), or
connection churn are unchanged; so are Ractor creation, GC and plain
threads (~50 configurations within +-2%).
make btest and full test-all pass with the feature on, as does btest on
a RUBY_DEBUG=1 build, and an adversarial fd close/reuse + cross-thread
close + fork-with-live-io stress. Platforms: epoll only; kqueue keeps
its current path (EV_DISPATCH would be the analogue). EPOLLONESHOT
predates every kernel that can build ruby (Linux 2.6.2), and an epoll
without it now falls back to USE_MN_THREADS=0 at compile time.
Co-Authored-By: Claude Opus 5 <
nor...@anthropic.com>
Modified files:
thread_pthread.c
thread_sched.h
thread_sched_mn.c