RubyとGoとBashで並行処理のパフォーマンス比較をしてみた

(image)RubyとGoとBashで並行処理のパフォーマンス比較をしてみた
目次

Rubyで書いたコードをGoで書き直したらどれくらい早くなるかを検証したくてやってみた。ついでにBashでもどうなるかも比較した。

画像ファイルをダウンロードするだけの単純な処理での比較。複雑な処理になるとまた結果は全然違ってくると思います。あしからず。

比較する処理内容

100個の画像をダウンロードするコードをサンプルにやってみた。

Rubyの場合

Ruby 2.6 で検証。

直列ダウンロード

まずは何も考えず順番に1つ1つダウンロードするRubyコード。

# no_thread.rb
require 'open-uri'

(1..100).each do |i|
  open("./images/#{i}.png", 'wb') do |f|
    f.write open("https://dummyimage.com/600x400").read
  end
end
$ time ruby no_thread.rb
ruby no_thread.rb  1.85s user 0.28s system 1% cpu 2:05.11 total
  • 実行時間: 約2分。遅い
  • CPU: 1%。CPUはほとんど使い切れていないことがわかる

Threadで並行ダウンロード

Threadを使って並行に処理してみる。

# thread.rb
require 'open-uri'

threads = []
(1..100).each do |i|
  threads << Thread.new do
    open("./images/#{i}.png", 'wb') do |f|
      f.write open("https://dummyimage.com/600x400").read
    end
  end
end
threads.map(&:join)
$ time ruby thread.rb
ruby thread.rb  1.64s user 0.28s system 71% cpu 2.670 total
  • 実行時間: 2.6秒。劇的な改善
  • CPU: 70%。うまくCPUも使えている

Goの場合

Goで書き直してみる。Goのバージョンは1.13を使用。

こちらのコードを参考に書いてみた。

// go.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	var url string = "https://dummyimage.com/600x400"

	for i := 0; i < 100; i++ {
		file, err := os.Create(fmt.Sprintf("images/%d.png", i))
		if err != nil {
			panic(err)
		}

		wg.Add(1)
		go func() {
			response, err := http.Get(url)
			if err != nil {
				panic(err)
			}
			defer response.Body.Close()
			defer file.Close()
			io.Copy(file, response.Body)
			wg.Done()
		}()
	}

	wg.Wait()
}

Go runで実行

コンパイルせずにgo runで実行してみる。

$ time go run go.go
go run go.go  0.79s user 0.37s system 25% cpu 4.532 total
  • 実行時間: 約4.5秒
  • CPU: 25%

build済みバイナリで実行

buildして実行する。

$ go build go.go
$ time ./go
./go  0.23s user 0.12s system 11% cpu 3.111 total
  • 実行時間: 約3秒。RubyのThreadコードより少し遅かった
  • CPU: 約10%。速度のわりにCPU消費が著しく低い!

Bashの場合

Bash(シェルスクリプト)の場合も試してみる。

# bash.bash
for i in `seq 1 100`; do
  wget --background --quiet "https://dummyimage.com/600x400" -O images/$i.png > /dev/null
done
$ time ./bash.bash
./bash.bash  0.25s user 0.30s system 43% cpu 1.241 total
  • 実行時間: 約1.2秒。速い!
  • CPU: 約40%

まとめ

単純な並行ダウンロードという処理において、実行速度だけでいうとRubyもThreadを使えばGoと同じ速度は出せるということがわかった。

一方でCPUコストに関してはビルド済みのGoバイナリのほうが圧倒的低く済むということが判明した。つまり複雑な計算処理においてはGoがRubyよりずっとアドバンテージがあるであろうことがわかった。

今回は単純な処理なのでRubyとGoの間で処理速度に大きな差は生まれなかったものの、比較するロジックが複雑化すればするほどRubyはGoに負けそうな予感がした。