Open9
GVL-less Ruby がほしい
GVL の制約を受けない Ruby がほしい。具体的には複数の Thread が parallel に動作できる Ruby がほしい。必要に応じて mutex などを記述することは受け入れる。
GVL の実体
Ruby 3.4 では rb_thread_sched が Thread の並行性制御を担っている、と思う。この辺をいじればとりあえず GVL を雑に外せるのではないか。
何をすれば GVL を外したことになるかよく分からないが、本来止まるべき Thread が止まらなければ目的は果たせる気がする。ので、こんなパッチをあててみる。
diff --git a/thread.c b/thread.c
index 2a937ca278..684efea3bc 100644
--- a/thread.c
+++ b/thread.c
@@ -1445,6 +1445,7 @@ rb_thread_sleep(int sec)
static void
rb_thread_schedule_limits(uint32_t limits_us)
{
+ return;
if (!rb_thread_alone()) {
rb_thread_t *th = GET_THREAD();
RUBY_DEBUG_LOG("us:%u", (unsigned int)limits_us);
diff --git a/thread_pthread.c b/thread_pthread.c
index c92fd52a66..8f39530581 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -384,6 +384,8 @@ ractor_sched_dump_(const char *file, int line, rb_vm_t *vm)
static void
thread_sched_lock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line)
{
+ return;
rb_native_mutex_lock(&sched->lock_);
#if VM_CHECK_MODE
@@ -398,6 +400,8 @@ thread_sched_lock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *f
static void
thread_sched_unlock_(struct rb_thread_sched *sched, rb_thread_t *th, const char *file, int line)
{
+ return;
RUBY_DEBUG_LOG2(file, line, "th:%u", rb_th_serial(th));
#if VM_CHECK_MODE
1つの Thread だけが存在する環境だと、意外にもごく普通に動作する。Ruby すごい。
並列で CPU を使うために、竹内関数を複数 Thread で実行してみる。12, 3, 0 はほとほどに時間がかかるパラメータ。
GC.disable
def takeuchi(x, y, z)
if x <= y
y
else
takeuchi(takeuchi(x-1, y, z), takeuchi(y-1, z, x), takeuchi(z-1, x, y))
end
end
ths = []
10.times do
ths << Thread.new {
takeuchi(12, 3, 0)
}
end
ths.each(&:join)
しっかり複数コアが使われている気がする。
が、しかし、100% ほど 遅く なってしまった。どうして……。
# 普通のRuby
% time /home/osyoyu/.rbenv/versions/master/bin/ruby ~/Development/tak.rb
/home/osyoyu/.rbenv/versions/master/bin/ruby ~/Development/tak.rb 1.28s user 0.00s system 98% cpu 1.303 total
# GVL-less Ruby
% time ./miniruby ~/Development/tak.rb
./miniruby ~/Development/tak.rb 23.98s user 0.00s system 910% cpu 2.634 total
竹内関数はメソッドをたくさん呼ぶので、その性質が良くないのかと思って、なるべくメソッド呼び出しを減らしたエラトステネスのふるいを実装。
GC.disable
def er(limit)
is_prime = Array.new(limit + 1, true)
is_prime[0] = false
is_prime[1] = false
i = 2
sq_limit = Math.sqrt(limit)
while i <= sq_limit do
if is_prime[i]
j = i * i
while j <= limit do
is_prime[j] = false
j += i
end
end
i += 1
end
nil
end
ths = []
10.times do
ths << Thread.new {
er(1000000)
}
end
ths.each(&:join)
しかしやはり倍ぐらい遅い。
# 普通のRuby
% time /home/osyoyu/.rbenv/versions/master/bin/ruby ~/Development/er.rb
/home/osyoyu/.rbenv/versions/master/bin/ruby ~/Development/er.rb 0.38s user 0.04s system 95% cpu 0.445 total
# GVL-less Ruby
% time ./miniruby ~/Development/er.rb
./miniruby ~/Development/er.rb 7.64s user 0.06s system 914% cpu 0.842 total
perf で見てもいまいち分からない。プロファイラってむずかしい
sudo perf record -F 99 -e task-clock --call-graph dwarf -- ./miniruby ~/Development/er.rb
perf をじっと見てたら Thread#join が時間を食っている気がする。
普通の Ruby と比べると vm_get_ep() が時間を大量に消費しているのも気になる。