Go vs Rust


どうも。首を刺して自■する寸前でとある企業に雇われ半年ほど経った者です。 ところで、Golangはマルチスレッド処理が手軽にできる言語ですが、何故か分かりませんがこの言語とRustはしばしば比較されます。

というわけで、どっちがパフォーマンスの良い言語なのか比較したいと思います。

比較を行った背景

何故か僕の周辺では何かに取り憑かれたように「RustいいよRust」と、無条件なRust賛美をよく目にするようになりました。でもぶっちゃけ、僕はどうして彼らがそこまでRustに心酔するのかよく分からないので、実際に検証コードなりを書いてRustを体験してみた・・・というのが背景です。

何を比較したか

  1. フィボナッチ数列を計算量$O(\log n)$で計算するコードをGoとRustで実装し、計算時間を測る。計算時間の短いほうをパフォーマンスの良い言語とする。
  2. 「Hello World」というテキストを返すウェブサーバーを実装し、abコマンド(Apache Benchmark)を使って秒あたりのスループットを計測した。スループットが高いほうの言語をパフォーマンスの良い言語とする。
  3. 1.及び2.のファイルサイズ

比較に使用した機器

自作マシン
    hyamamoto@hyamamoto-home (pts/3)    ~/Documents/Workspace/go-vs-rust     master ?1  archey3                                                  ✔  13:18:19 

               +                OS: Arch Linux x86_64
               #                Hostname: hyamamoto-home
              ###               Kernel Release: 5.7.7-arch1-1
             #####              Uptime: 2:44
             ######             WM: KWin
            ; #####;            DE: KDE
           +##.#####            Packages: 1493
          +##########           RAM: 5311 MB / 32085 MB
         #############;         Processor Type: AMD Ryzen 7 3700X 8-Core Processor
        ###############+        $EDITOR: vim
       #######   #######        Root: 39G / 457G (8%) (ext4)
     .######;     ;###;`".
    .#######;     ;#####.
    #########.   .########`
   ######'           '######
  ;####                 ####;
  ##'                     '##
 #'                         `#

比較するアプリの前提

  • アプリはリリースされる事を前提とする。つまり、標準のコンパイラの機能を使って最適化処理が可能であればこれを行う。(i.e. rust版アプリには--releaseフラグがつくよ!!)

検証コード

比較に使用したコードはGithubにあります。

また、計算にはGoの場合はmath/big、Rustの場合はrugという多倍長数値演算ライブラリを使用します。

比較1. フィボナッチ数列

フィボナッチ数列を生成する問題は各社のコーディングテストや言語のベンチマーク等では定番中の定番です。単純実装の場合の時間計算量は$O(2^n)$、計算後の状態を次の計算に利用する実装(メモ化)では$O(n)$、2x2の行列を用いて計算する実装では$O(\log n)$となります。

今回の場合、最も計算量が少ない方法で実装するので、コンパイラの最適化能力が直に顕れる・・・と思います。

計測結果

以下が計測の結果です。「Go」のラベルがあるものはGoで書かれた実装のもの、「Rust」のラベルがあるものがRustで書かれた実装のものです。今回は、$2^{20} = 1,048,576$番目のフィボナッチ数列を計算します。

Go
    hyamamoto@hyamamoto-home (pts/3)    ~/Doc/W/go-vs-rust/go     master !1 ?4  time ./bin/fib 1048576                                           ✔  13:39:25 
(Super big number)
./bin/fib 1048576  198.51s user 24.31s system 180% cpu 2:03.65 total
Rust
    hyamamoto@hyamamoto-home (pts/1)    ~/Doc/W/go-vs-rust/rust     master !1 ?4  time ./target/release/fib 1048576                     ✔  28s    13:47:38 
(Super big number)
./target/release/fib 1048576  25.65s user 1.40s system 99% cpu 27.083 total

Rustの圧勝 Goは並列化によってほぼ2個分のCPUを使っているように見えるにも関わらず、トータルでの計算時間は2分程度であることに対して、RustはCPUを1個しか使っていないにも関わらず、計算時間は30秒程度しか掛かっていません。つまり、フィボナッチ数列の計算だけ、言い換えれば単純なループでも結構な差が出ている事が分かりました。

・・・と、言いたい所ですが、後々、math/bigrugのコードを調べた所、math/bigでは多倍長整数の乗算にKaratsuba法を、rugではGNU MPを使っている、つまり、FFTを用いて時間計算量を最適化しています。FFTによる乗算はKaratsuba法より計算量が少なくなるので、そりゃぁRustが圧勝するわな😅

というわけで、Goで書かれたコードでもGMPを利用するハイレベルコードを使用して、再度計測を行った結果がこちらになります:

go_revised
   hyamamoto@hyamamoto-home (pts/1)    ~/Doc/W/go-vs-rust     master !4 ?1  time ./go/bin/fib_gmp 1048576                                       ✔  09:17:28 
./go/bin/fib_gmp 1048576  72.74s user 13.31s system 117% cpu 1:13.52 total

計算時間は1分10秒程度まで短縮できましたが、やはりRustの約30秒という速さには負けるようです。

比較2: Webサービス

次に、「Hello World」という文字列を表示するだけの非常に単純なWebサービスを実装し、そのスループットを比較しました。Goではデフォルトのnet/httpを、Rustではwarpと呼ばれるライブラリを使用し、またベンチマークツールにはApache Benchmarkを使用しました。:

Go
    hyamamoto@hyamamoto-home (pts/3)    ~/Doc/W/go-vs-rust/go     master !1  ab -n 16000000 -c $(nproc) 'http://localhost:5000/'              22 ✘  14:19:11 
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1600000 requests
Completed 3200000 requests
Completed 4800000 requests
Completed 6400000 requests
Completed 8000000 requests
Completed 9600000 requests
Completed 11200000 requests
Completed 12800000 requests
Completed 14400000 requests
Completed 16000000 requests
Finished 16000000 requests


Server Software:
Server Hostname:        localhost
Server Port:            5000

Document Path:          /
Document Length:        11 bytes

Concurrency Level:      16
Time taken for tests:   551.510 seconds
Complete requests:      16000000
Failed requests:        0
Total transferred:      2048000000 bytes
HTML transferred:       176000000 bytes
Requests per second:    29011.27 [#/sec] (mean)
Time per request:       0.552 [ms] (mean)
Time per request:       0.034 [ms] (mean, across all concurrent requests)
Transfer rate:          3626.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       4
Processing:     0    0   0.1      0      11
Waiting:        0    0   0.1      0      10
Total:          0    1   0.1      1      11

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      1
  99%      1
 100%     11 (longest request)
Rust
    hyamamoto@hyamamoto-home (pts/3)    ~/Doc/W/go-vs-rust/go     master !1  ab -n 16000000 -c $(nproc) 'http://localhost:5000/'     ✔  9m 22s    14:28:43 
This is ApacheBench, Version 2.3 <$Revision: 1874286 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 1600000 requests
Completed 3200000 requests
Completed 4800000 requests
Completed 6400000 requests
Completed 8000000 requests
Completed 9600000 requests
Completed 11200000 requests
Completed 12800000 requests
Completed 14400000 requests
Completed 16000000 requests
Finished 16000000 requests


Server Software:
Server Hostname:        localhost
Server Port:            5000

Document Path:          /
Document Length:        11 bytes

Concurrency Level:      16
Time taken for tests:   527.022 seconds
Complete requests:      16000000
Failed requests:        0
Total transferred:      2048000000 bytes
HTML transferred:       176000000 bytes
Requests per second:    30359.25 [#/sec] (mean)
Time per request:       0.527 [ms] (mean)
Time per request:       0.033 [ms] (mean, across all concurrent requests)
Transfer rate:          3794.91 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       2
Processing:     0    0   0.1      0      11
Waiting:        0    0   0.1      0      11
Total:          0    1   0.1      0      11
ERROR: The median and mean for the total time are more than twice the standard
       deviation apart. These results are NOT reliable.

Percentage of the requests served within a certain time (ms)
  50%      0
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      1
  99%      1
 100%     11 (longest request)

Goで書かれたコードのスループットは約29.0k req/secである事に対して、Rustで書かれたコードのスループットは約30.4k req/secでした。Rustのが若干速いが、ほぼ差は無いように見えます。では何が原因なのかというと、恐らく実装が単純すぎたため、最終的にIOのレイテンシーの勝負になってしまったのではないかと思います。i.e. 数MB程度のバイナリデータを圧縮して出力するような計測方法を取れば有意な差は出たかも?

ファイルサイズ

Go
    hyamamoto@hyamamoto-home (pts/3)    ~/Doc/W/go-vs-rust     master !1  ls -lh go/bin                                                          ✔  14:53:11 
total 9.4M
-rwxr-xr-x 1 hyamamoto hyamamoto 2.2M Jul 11 13:39 fib
-rwxr-xr-x 1 hyamamoto hyamamoto 7.2M Jul 11 13:39 http
Rust
    hyamamoto@hyamamoto-home (pts/3)    ~/Doc/W/go-vs-rust     master !1  ls -lh rust/target/release                                             ✔  14:53:17 
total 7.8M
-rwxr-xr-x  2 hyamamoto hyamamoto 3.1M Jul 11 13:46 fib
-rwxr-xr-x  2 hyamamoto hyamamoto 4.7M Jul 11 13:46 http

フィボナッチ数列のコードのバイナリはGoのほうがRustよりもコンパクトであることに対して、ウェブサーバーのコードのバイナリはRustの方がコンパクトです。最適化によってデバッグシンボルと不要なコードが削除されているのが効いているように見えます。

結論

上記の結果から考察するに、Rustの利用が向いている分野として、恐らく計算に掛かるレイテンシーを低く抑えたい分野、例えばPied Piperのような圧縮アルゴリズムを作ったりだとか、あるいはDBMSの実装など、比較的に低レベルな領域の実装には向いている言語ではないかと思います。対して、Webの場合では大抵のレイテンシーの問題はIOであり、またWebの圧縮技術はCDNやnginx等が実装してくれているため、単純にDBからデータを取ってそれをJSONなどにして出力するといったものでは、バイナリサイズが問題とならない限りはRustで実装するメリットはあまり感じられないかと思います。

しかし!!僕は敢えて言おう!!

Rust良いよRust!

結局どちらを学ぶべきか

勿論どちらもです。ただし、Goの方が構造がシンプルなので、まずはGoから手を付けられる事をオススメします。