この記事を書くにあたって
アプリケーション、パッケージやコマンドラインの検証等でベンチマークを測定する際、
shell組み込みのtimeコマンドを使うことが自分は多い。なお /usr/bin/time
のGNU time を使うことで実行時間だけでなくCPUや平均・最大使用メモリも測定できるが、今回はその話題に触れない。
私が感じるhyperfineの魅力は、ベンチマークの比較ができること、warmupが指定できることである。それらは以下で解説する。
hyperfine とは
Rust製のベンチマークコマンドである。installは cargo
や brew
などが簡単であろう。
See Installation
主な使い方
timeコマンドとは異なり、ベンチマークの実行対象となるコマンドは '文字リテラル'
で入れる必要がある。
# ❌
hyperfine sleep 0.3
# ✅
hyperfine 'sleep 0.3'
デフォルトの実行回数は10回だが、--runs
(or -r
) で回数を指定できる。
hyperfine --runs 5 'sleep 0.3'
コマンドの比較
大抵のベンチマークを取る目的は、コードの改善などの比較のためにおこなわれる。hyperfine は二つコマンドを取ると、それぞれの結果と比較の出力する。以下は全く面白みのないベンチマークの比較を記載する。
hyperfine -r 5 'sleep 0.2' 'sleep 0.3'
Benchmark 1: sleep 0.2
Time (mean ± σ): 202.4 ms ± 0.1 ms [User: 2.0 ms, System: 0.5 ms]
Range (min … max): 202.2 ms … 202.6 ms 5 runs
Benchmark 2: sleep 0.3
Time (mean ± σ): 302.9 ms ± 0.3 ms [User: 2.6 ms, System: 0.6 ms]
Range (min … max): 302.6 ms … 303.4 ms 5 runs
Summary
sleep 0.2 ran
1.50 ± 0.00 times faster than sleep 0.3
なお比較は2個以上も可能である。
hyperfine -r 3 'sleep 0.1' 'sleep 0.15' 'sleep 0.2' 'sleep 0.25'
Benchmark 1: sleep 0.1
Time (mean ± σ): 103.0 ms ± 1.1 ms [User: 2.4 ms, System: 0.8 ms]
Range (min … max): 101.7 ms … 103.9 ms 3 runs
Benchmark 2: sleep 0.15
Time (mean ± σ): 153.6 ms ± 1.1 ms [User: 2.4 ms, System: 1.3 ms]
Range (min … max): 152.8 ms … 154.8 ms 3 runs
Benchmark 3: sleep 0.2
Time (mean ± σ): 203.8 ms ± 1.2 ms [User: 3.0 ms, System: 0.9 ms]
Range (min … max): 202.4 ms … 204.7 ms 3 runs
Benchmark 4: sleep 0.25
Time (mean ± σ): 252.4 ms ± 0.8 ms [User: 2.6 ms, System: 0.0 ms]
Range (min … max): 251.6 ms … 253.2 ms 3 runs
Summary
sleep 0.1 ran
1.49 ± 0.02 times faster than sleep 0.15
1.98 ± 0.02 times faster than sleep 0.2
2.45 ± 0.03 times faster than sleep 0.25
warmup
さて、hyperfine が time コマンドより便利であると感じたのは、 --warmup
(or -w
) コマンドである。ディスクからの読み込みなど I/O の影響がある場合は warmup により(CPUなど部分的に)cacheされるため、I/O 部分の影響を(厳密ではないが)排除できる。
例えば DuckDBの read_parquet にてデータの読み込みについて、--warmup指定無しの場合(おそらく最初の1回目が) Range max の値となり、平均値はそれに引きずられていると思われる。
hyperfine --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"'
# Benchmark 1: duckdb -c "FROM read_parquet('lineitem.parquet');"
# Time (mean ± σ): 530.9 ms ± 72.1 ms [User: 5626.9 ms, System: 1379.5 ms]
# Range (min … max): 486.7 ms … 658.2 ms 5 runs
-w 1
と warmup を1回入れて実行すると、Rangeも安定的であるので、ベンチマーク測定1回目からcacheが効いていると推測される。ことが確認できる。
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"'
# Benchmark 1: duckdb -c "FROM read_parquet('lineitem.parquet');"
# Time (mean ± σ): 488.5 ms ± 10.7 ms [User: 5478.7 ms, System: 1205.1 ms]
# Range (min … max): 478.5 ms … 504.9 ms 5 runs
逆にI/Oの影響を測定したい、すなわち cold start をする場合は --prepare
というオプションがある。公式での例を参照されたい
出力フォーマット
以下の指定が可能である
--export-markdown $FILENAME
--export-json $FILENAME
--export-csv $FILENAME
出力フォーマットは以下のようになる。json形式が個別の測定値まで持ち一番情報量が多い。
結果を可視化するには json形式で出力し、公式の python script から plot_whisker.py
等をローカルにダウンロードして実行すればよい。ここでも uv
の威力が発揮する。
uv run plot_whisker.py bench-out.json
以下 --export-* の実行結果
markdown
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-markdown /dev/stdout
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|:---|---:|---:|---:|---:|
| `duckdb -c "FROM read_parquet('lineitem.parquet');"` | 518.3 ± 18.8 | 494.4 | 541.8 | 1.00 |
json
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-json /dev/stdout
{
"results": [
{
"command": "duckdb -c \"FROM read_parquet('lineitem.parquet');\"",
"mean": 0.52732588448,
"stddev": 0.04589734886338472,
"median": 0.51568904428,
"user": 5.718178199999999,
"system": 1.4224087800000003,
"min": 0.49069895828000004,
"max": 0.60658191028,
"times": [
0.51568904428,
0.50214623628,
0.60658191028,
0.52151327328,
0.49069895828000004
],
"exit_codes": [
0,
0,
0,
0,
0
]
}
]
}
csv
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-csv /dev/stdout
command,mean,stddev,median,user,system,min,max
"duckdb -c ""FROM read_parquet('lineitem.parquet');""",0.5254087186600002,0.03960067567439999,0.51560465546,5.860614059999999,1.2543148,0.49691412846000005,0.59473722746
以上