思い立って、HaskellとOCamlそれぞれでWebアプリを開発するにあたって、どれくらいの性能が出る ものなのか、まずはベンチマークをとってみることにした。フレームワークとして、Haskellに ついてはSpock, OCamlについてはOpium を選んだ。

Spockは、GHC7.6.3とcabal-installを使用してビルドした。Spockは0.10.0.1を使用した。 まずは、src/Main.hsを示す(ほぼGitHubにあるもののまま)。

{-# LANGUAGE OverloadedStrings #-}
import Web.Spock

import qualified Data.Text as T

main =
  runSpock 7777 $ spockT id $
    do get ("echo" <//> var) $ \something ->
        text $ T.concat ["Echo: ", something]

こちらをcabal buildしてから

$ ab -n 10000 -c 100 'http://127.0.0.1:7777/echo/world'

としてテストしてみた。代表的な結果は次のような感じになった。

Server Software:        Warp/3.2.6
Server Hostname:        127.0.0.1
Server Port:            7777

Document Path:          /echo/world
Document Length:        11 bytes

Concurrency Level:      100
Time taken for tests:   0.991 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1280000 bytes
HTML transferred:       110000 bytes
Requests per second:    10086.93 [#/sec] (mean)
Time per request:       9.914 [ms] (mean)
Time per request:       0.099 [ms] (mean, across all concurrent requests)
Transfer rate:          1260.87 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.5      0      21
Processing:     0    9   3.9      9      53
Waiting:        0    9   3.8      9      53
Total:          0   10   4.7      9      67

Percentage of the requests served within a certain time (ms)
  50%      9
  66%      9
  75%     10
  80%     10
  90%     11
  95%     13
  98%     20
  99%     22
 100%     67 (longest request)

次に、OpiumについてはopamでOCamlコンパイラとしては4.03.0を使用した。これまたほぼGitHubのサンプルから、以下のような プログラムを作成した。

open Opium.Std

type person = {
  name: string;
  age: int;
}

let json_of_person { name ; age } =
  let open Ezjsonm in
  dict [ "name", (string name)
       ; "age", (int age) ]

let print_param = get "/hello/:name" begin fun req ->
  `String ("Hello " ^ param req "name") |> respond'
end

let print_person = get "/person/:name/:age" begin fun req ->
  let person = {
    name = param req "name";
    age = "age" |> param req |> int_of_string;
  } in
  `Json (person |> json_of_person) |> respond'
end

let _ =
  App.empty
  |> print_param
  |> print_person
  |> App.run_command

GitHubのページで説明されている通りに

$ ocamlbuild -pkg opium.unix hello_world.native

としてコンパイルした。先ほどと同様にabでテストする。

$ ab -n 10000 -c 100 'http://127.0.0.1:3000/hello/world'

代表的な結果は次のような感じになった。

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname:        127.0.0.1
Server Port:            3000

Document Path:          /hello/world
Document Length:        11 bytes

Concurrency Level:      100
Time taken for tests:   1.445 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      500000 bytes
HTML transferred:       110000 bytes
Requests per second:    6918.94 [#/sec] (mean)
Time per request:       14.453 [ms] (mean)
Time per request:       0.145 [ms] (mean, across all concurrent requests)
Transfer rate:          337.84 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    6  75.2      0    1000
Processing:     1    3  25.2      2    1020
Waiting:        1    3  25.2      2    1020
Total:          1    9  95.7      2    1409

Percentage of the requests served within a certain time (ms)
  50%      2
  66%      2
  75%      2
  80%      2
  90%      2
  95%      2
  98%      5
  99%     18
 100%   1409 (longest request)

データ転送量も同一ではないので、全く同じ土俵での比較ではない(Spockの方がTotal transferredは多い)ものの、 次のような傾向が見えてきた。

  • Requests per secondについては、Spockの方が良い。
  • Transfer rateについても、Spockの方が良い。
  • 経過時間の中央値(median)については、Opiumの方が良い。
  • 要求の95-98%程度まではOpiumの方が早く処理できているが、ごく一部のリクエストについて、極端に 処理時間が伸びる場合がある(1.4秒もかかっている場合もある)。 一方、Spockは最悪値でも67ミリ秒なので、偏差は小さく安定している。

個人的には上記の最後の点が気になった。Opiumは何回ベンチマークをしてみても"ひっかかる"。OCamlのRTSなどは 全くいじっていないので、パラメタを調整することでもっと安定した性能が出るのかもしれない。 逆にSpockの安定感は素晴らしいと思った。Measuring GC latencies in Haskell, OCaml, Racketによれば、(今回とはソフトウェアの性質は全く異なるが)OCamlのGCの レイテンシが低いということが傾向としてあるようだ。そのため、逆の結果になるかと思っていたので意外だった。

Comments