以下のコードをそれぞれちょいとベンチしてみました。
Nim
import asynchttpserver, asyncdispatch const port = 8080 let server = newAsyncHttpServer(true, true) waitFor server.serve(Port(port), proc (req: Request) {.async.} = await req.respond( Http200, "Hello world", newHttpHeaders([("Content-Type", "text/plain")]) ) )
golang
package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello world") } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
Crystal
require "http/server" port = 8080 server = HTTP::Server.new(port) do |context| context.response.content_type = "text/plain" context.response.print "Hello world" end server.listen
Node.js
var http = require('http'); const port = 8080 http.createServer(function (req, res) { res.writeHead(200, { "Content-Type": "text/plain" }) res.end("Hello world") }).listen(port)
ab -c 400 -n 4000 でテスト
言語 | ファイルサイズ | メモリ | 結果(req/sec) |
---|---|---|---|
Nim | 237KB | 812KB | 4,300 ~ 4,800 |
golang | 6.8MB | 2.3MB | 4,300 ~ 4,800 |
Crystal | 2.8MB | 2.3MB | 4,300 ~ 4,800 |
Node.js | 186B | 18MB | 2,500 ~ 3,200 |
速度的には Nim、golang、Crystal それぞれどっこいどっこいと言った感じです。Node.js はさすがにその他3つと比べると遅いです。
メモリはさすがに Nim が一番食いません。ファイルサイズはそりゃコンパイルもへったくれもない単なるスクリプトの Node.js が一番小さいですね。Nim もコンパイルされたものの中ではかなり小さいです。
Node.js と Nim は、シングルプロセスで多重 IO を使っているだけなので1つ処理が引っかかってしまうとその他全てが足を引っ張られます。そうならないためには fork か thread で処理を分散させてやる必要があります。Node.js の場合は cluster などを使うと簡単にできます。Nim の場合は以下のような感じでできます。
import asynchttpserver, asyncdispatch, osproc, posix const port = 8080 proc response(req: Request) {.async.} = await req.respond(Http200, "Hello world\n") proc forkChild = var server = newAsyncHttpServer(true, true) asyncCheck server.serve(Port(port), response) runForever() proc main = let procNum = countProcessors() for i in 0..<procNum: let pid = fork() if pid == 0: forkChild() quit(0) while true: discard main()
CPU の個数分 fork しています。これは簡易的なもので、子プロセスが死んでも復活しません。親プロセスは知らんぷりです。ちゃんとやる時は親で SIGCHILD を受け取ってちょろちょろやる必要があります。その内詳しく書きます。
Crystal と golang は thread で勝手に並列処理してくれます。
ただ、Crystal はコンパイルが遅くてかなりイライラします。release でコンパイルすると、あり得ないレベルになります。
Nim は最初の1回目は遅いんですが 2回目以降はいじったファイル以外は cache が効くので速いんです。
何故そうなるのかは、make が何者か知っている人であればコンパイルを実行したディレクトリに出来上がる ninmcache ディレクトリを見ると一発でわかると思います。
知らない人向けに一応書いておくと、Nim でコンパイルすると、Nim で書かれたコードがファイル毎に C言語に変換されて nimcache ディレクトリに格納されます(hoge.nim が hoge.c に)。
その hoge.c がコンパイルされてオブジェクトファイルと呼ばれる中間ファイルになります(hoge.c が hoge.o に)。
コード中で呼んだライブラリも全て同じように .c と .o になって nimcache に格納されます。
これらのオブジェクトファイル全てをリンカってやつががっちゃんこして実行ファイルが作られるわけです。
再コンパイル時には、いじったファイルだけが再コンパイルされ、いじってないものは以前のオブジェクトファイルそのままでがっちゃんこされます。こうしてコンパイルする量を減らすことで全体のコンパイル速度を上げているわけです。これこそが make のお仕事だったりします。
golang は今回のコードが少ないおかげでコンスタントに速いですが、オブジェクトファイルなどはないので、書いているコードが増えると遅くなっていきます。
言語 | 1回目(sec) | 2回目(sec) |
---|---|---|
Nim | 3.2 | 1.0 |
Nim(release) | 5.1 | 0.8 |
golang | 1.0 | 1.0 |
Crystal | 3.3 | 3.2 |
Crystal(release) | 44 | 44 |
速度、メモリ使用量などなど、全部ひっくるめて考えると、やはり Nim が最強ですね(Nim バカ談)。