👌

Bun, Deno, Go, Node, Rust, Zig: A Benchmark

2023/02/07に公開
9

Bun, Deno, Go, Node, Rust, Zig: A Benchmark

vim-jp slack の #lang-go で、Go vs Node が土日に繰り広げられていました(月曜日気づいた)。
mattn さんが、Go と Node の速度を比較するベンチマークを書いていたので、それを bun, deno, go, node, rust, zig で書いてみました。
(zig わからないので未完成です 🙇)

https://github.com/ekusiadadus/bench-web-server

ベンチマーク

Language Requests per second Time per request
bun 11793.40 [#/sec] (mean) 0.848 [ms] (mean)
deno 32913.58 [#/sec] (mean) 0.304 [ms] (mean)
go 85736.82 [#/sec] (mean) 0.117 [ms] (mean)
node 11187.35 [#/sec] (mean) 0.894 [ms] (mean)
rust 20267.08 [#/sec] (mean) 0.493 [ms] (mean)
zig 未測定 未測定

ということで、Go が一番速いです。

Go >> Deno > Rust > Bun > Node という結果になりました。

Bombardier (追記)

Zenn にも投稿したら、別のベンチマーカーを教えていただいたので試しました

https://github.com/codesenberg/bombardier

Language Reqs/sec Avg Latency Avg
bun 87817.82 111.74us
deno 39247.26 252.97us
go 59071.51 166.08us
node 17808.81 567.93us
rust 36398.10 271.31us
rust (release) 36390.67 271.66us
rust (multi-thread) 36014.16 274.18us
hyper 75011.92 131.11us
zig 未測定 未測定

このベンチマークだと、
Bun > hyper > Go > Deno > Rust > Node になりました

注意

この計測は、特定の言語やフレームワークを批判するものではないです。
それぞれの言語やフレームワークには、それぞれの良いところ、悪いところがあると思っています。
また、計測方法や各言語の最適化ができていないと思います。
間違いがあったときは申し訳ございません。

ベンチマークの実行

install

sudo apt install apache2-utils

run

ab -k -c 10 -n 10000 http://127.0.0.1:3000/

ベンチマークのコード

Go

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Content-Type", "text/html")
		fmt.Fprintf(w, "<h1>Hello World</h1>")
	})
	http.ListenAndServe(":3000", nil)
}

Rust

use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;

fn main() {
		let listener = TcpListener::bind("127.0.0.1:3000").unwrap();
		for stream in listener.incoming() {
				let stream = stream.unwrap();
				handle_connection(stream);
		}
}

fn handle_connection(mut stream: TcpStream) {
		let mut buffer = [0; 512];
		stream.read(&mut buffer).unwrap();
		let response = "HTTP/1.1 200 OK\r\n\r\n<h1>Hello World</h1>";
		stream.write(response.as_bytes()).unwrap();
		stream.flush().unwrap();
}

Zig

const std = @import("std");
const net = std.net;
const StreamServer = net.StreamServer;
const Address = net.Address;
pub const io_mode = .evented;

pub fn main() anyerror!void {
  var stream_server = StreamServer.init(.{});
  defer stream_server.close();
  const address = try Address.resolveIp("127.0.0.1", 3000);
  try stream_server.listen(address);
  while (true) {
    const client = try stream_server.accept();
      const response = "HTTP/1.1 200 OK\r\n\r\n<h1>Hello World</h1>";
      try client.write(response);
    }
}

Node

const http = require("http");
const port = 3000;

const server = http.createServer((request, response) => {
  response.writeHead(200, {
    "Content-Type": "text/html",
  });
  const responseMessage = "<h1>Hello World</h1>";
  response.end(responseMessage);
});

server.listen(port);

Deno

const server = Deno.listen({ port: 3000 });

for await (const conn of server) {
  serveHttp(conn);
}

async function serveHttp(conn: Deno.Conn) {
  const httpConn = Deno.serveHttp(conn);
  for await (const requestEvent of httpConn) {
    const body = "<h1>Hello World</h1>";
    requestEvent.respondWith(
      new Response(body, {
        status: 200,
      })
    );
  }
}

Bun

// TypeScript: http.ts
export default {
  port: 3000,
  fetch(request: Request) {
    return new Response("Hello World");
  },
};

Makefile

.phony:

build-go:
	go build go/main.go && ./main

build-rust:
	rustc rust/main.rs && ./main

build-zig:
	zig build-exe zig/main.zig && ./main

run-go:
	go run go/main.go

run-node:
	node node/main.js

run-bun:
	bun run bun/main.ts

run-deno:
	deno run --allow-net deno/main.ts

bench:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/

bench-go:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/go.txt

bench-rust:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/rust.txt

bench-node:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/node.txt

bench-bun:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/bun.txt

bench-deno:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/deno.txt

bench-zig:
	ab -k -c 10 -n 10000 http://127.0.0.1:3000/ > bench/zig.txt

check-port:
	echo 'sudo lsof -i :3000'
GitHubで編集を提案

Discussion

tetsu_kobatetsu_koba

計測ありがとうございます。
この記事は今後長く参照されることになると思いますので、各処理系のバージョンを明記しておいて他の方が同じ条件で追試を行うことができるようにするとよいと思います。
また、それぞれについてのコンパイルで最適化のオプションがついていないようなので、それによる違いも調べてみたらどうでしょう。 特にrustはデバッグビルドとリリースビルドで速度にかなりの差があるというのを聴いたことがあります。

ekusiadadusekusiadadus

ありがとうございます。
後ほど追記します🙇

Rust の場合ですが、そもそも TCPListener でのシングルスレッド処理なので遅いのは仕方ないのかなという気持ちです。非同期処理したいなら、パッケージ入れるしかないかなと...
rustc でコンパイルしているので、後で最適化したものも追記します。

yusukebeyusukebe

こんにちわ。Node/Deno/Bunのベンチマークをよくとってる者です。abは遅いので、他のベンチマークツール使ったほうがいいと思います。例えば、Bunはこの辺、かなりシビアにベンチマークとってて、Bombardierというツールを推奨しています。

https://github.com/codesenberg/bombardier

コマンドは

bombardier --fasthttp -d 10s -c 150 'http://localhost:3000/'

とかやります。

あと、DenoはDeno flashというdeprecatedになりそうですが、Deno.serveコマンドで起動するサーバーを使った方が速かったり、DenoもBunもContent-Typeが書かれてなかったりするので、直しておくといいかもしれません!同じような趣旨でDenoのフレームワーク等と比べてるレポジトリがあるので貼っておきます。

https://github.com/denosaurs/bench

ekusiadadusekusiadadus

貴重な情報ありがとうございます🙇

ab 遅いのですか...
ベンチマークはどれを使うべきなのか、かなり困っていました..
助かります

土日に試してみます!

yusukebeyusukebe

そうっすねー。Bun速いっす。Denoも、Deno.serve使うとBunほどじゃないけど速いです。

await Deno.serve(() => new Response("Hello, Bench!"), {
  port: 8000,
});

JSランタイムだと現状はBun、Deno、Node.jsの順になりますね。

ekusiadadusekusiadadus

html 配布とかでもう一回ベンチしてみようかなーと思いました

もしご存じだったらなのですが、ベンチまとめたリポジトリとかって知ってます?
現状、shellの自作のしかないので...

https://github.com/ekusiadadus/my-bench

yusukebeyusukebe

HTTPサーバーだったら上記のDenoやつとBunのフレームワークをベンチしてるこちらのレポジトリが参考になります。

https://github.com/SaltyAom/bun-http-framework-benchmark

JS/TSになっちゃいますが、ベンチマークスクリプトがよくて自動でスコアが書かれたMarkdown作ってくれたりします。あとはGoなら似たようフレームワーク比較するのがありそうっすね。