Open9

GVL-less Ruby がほしい

osyoyuosyoyu

GVL の制約を受けない Ruby がほしい。具体的には複数の Thread が parallel に動作できる Ruby がほしい。必要に応じて mutex などを記述することは受け入れる。

osyoyuosyoyu

GVL の実体

Ruby 3.4 では rb_thread_sched が Thread の並行性制御を担っている、と思う。この辺をいじればとりあえず GVL を雑に外せるのではないか。

osyoyuosyoyu

何をすれば 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
osyoyuosyoyu

1つの Thread だけが存在する環境だと、意外にもごく普通に動作する。Ruby すごい。

osyoyuosyoyu

並列で 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
osyoyuosyoyu

竹内関数はメソッドをたくさん呼ぶので、その性質が良くないのかと思って、なるべくメソッド呼び出しを減らしたエラトステネスのふるいを実装。

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
osyoyuosyoyu

perf で見てもいまいち分からない。プロファイラってむずかしい

sudo perf record -F 99 -e task-clock --call-graph dwarf -- ./miniruby ~/Development/er.rb
osyoyuosyoyu

perf をじっと見てたら Thread#join が時間を食っている気がする。

osyoyuosyoyu

普通の Ruby と比べると vm_get_ep() が時間を大量に消費しているのも気になる。