Rgotで並列/並行ベンチマーク

前回前々回にひきつづき、コツコツつくっているgem Rgotはgo testを模しているので当然Benchmark機能をもっています。

go testコマンドでは-cpuオプションをつけると、並列するgoroutineの数と、runtime.GOMAXPROCSで設定できる使うCPUの数をオプションで指定でき、並列化によって性能が向上しているのかBenchmarkをとることができます。

こちらもパク参考にさせていただき、Rubyにおける並列性のBenchmarkが取れるように実装しました。 ついでに並行性のBenchmarkもやっておきました。

タテマエ

Rubyでは並列動作させるにはforkしてプロセスを作ることになります。 また、並行動作させるにはthreadを作ることになります。

Rgotでは、--cpuオプションでforkするプロセスの数を、 --threadでThread.newで作るthreadの数をそれぞれ調整できるようになっています。

$ rgot --cpu=2,4 --thread=2,4
foo_test-2(2)  100 100 ns/op
foo_test-2(4)  100 100 ns/op
foo_test-4(2)  100 100 ns/op
foo_test-4(4)  100 100 ns/op

このように、プロセスが2個のときと4個のとき、threadが2個のときと4個のときの全ての組み合わせをBenchmarkできます。 -2となっているのがプロセス数、(2)がthread数を表しています。

その次の数字はこれだけのタスクをこなしたと読めばOK.

ns/opはイチ動作あたり何ナノ秒かかったかを示しています。

構文の翻訳

構文はgoだと

func BenchmarkFoo (b *testing.B) {
  b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
      fibo(27)
    }
  })
}

のような感じですが、これをRubyっぽくブロック構文を使って

def benchmark_foo(b)
  b.run_parallel do |pb|
    while pb.next
      fibo(27)
    end
  end
end

というように翻訳しました。

実践

さっそく試してみます。

フィボナッチ数の計算と、sleepで並列/並行性能を測ってみました。

module BenchmarkTest
  def fibo(n)
    if n < 2
      n
    else
      fibo(n - 1) + fibo(n - 2)
    end
  end

  def benchmark_fibo(b)
    b.run_parallel do |pb|
      while pb.next
        fibo(27)
      end
    end
  end

  def benchmark_sleep(b)
    b.run_parallel do |pb|
      while pb.next
        sleep 0.1
      end
    end
  end
end
$ rgot benchmark_test.rb --bench . --cpu=2,4 --thread=2,4
benchmark_fibo-2(2)     160     13083452.156 ns/op
benchmark_fibo-2(4)     320     13029861.872 ns/op
benchmark_fibo-4(2)     640     7369437.953 ns/op
benchmark_fibo-4(4)     1280    7677284.290 ns/op
benchmark_sleep-2(2)    80      26198324.537 ns/op
benchmark_sleep-2(4)    320     12980783.569 ns/op
benchmark_sleep-4(2)    320     12982687.494 ns/op
benchmark_sleep-4(4)    1280    6472016.920 ns/op
ok  BenchmarkTest   40.527s

CPUバウンスなフィボナッチ数を計算する場合は、プロセス数が増えると性能が向上していることがわかりますが、スレッド数が増えても性能の向上は見られません。

Rubyのスレッドは同時に動けるのは1つだけという制限があるので、並列に使えるCPUの数はプロセス数に依存するためだとBenchmarkから考察できます。

また、I/Oバウンスな(処理を模した)sleepでは、プロセス数が増えてもthread数が増えても性能が向上しています。

RubyのThreadは、並列では動けないとはいえ並行では動けるので、sleepやHTTPリクエストなどCPUを使わないブロッキングがある処理では効果を発揮することがBenchmarkから読み取ることができます。

ちなみに

同等のコードをgolangで書いてみた場合のBenchmarkです

$ go test -bench . -cpu=4,8,16
testing: warning: no tests to run
PASS
BenchmarkFibo-4         5000        293746 ns/op
BenchmarkFibo-8         5000        268685 ns/op
BenchmarkFibo-16        5000        280306 ns/op
BenchmarkSleep-4          50      26219605 ns/op
BenchmarkSleep-8         100      13097673 ns/op
BenchmarkSleep-16        200       6561824 ns/op
ok      github.com/ksss/gotest  9.155s

golang速い……。

というわけで

RgotをつかったRubyの並行/並列ベンチマークの書き方でした。

並列/並行ベンチマークはRgot v0.0.5から使用可能です。

github.com