TechQuant Blog

hyperfine 実践Tips — CLI コマンドの速度を統計的に測るベンチマークツール

7分で読める

「このスクリプト、なんか遅くなった気がする」。そう感じて time を 3 回叩いて、毎回バラバラな数字を見て、結局 Slack に「うーん、誤差かも?」と書いて終わる。先週、自分はそれをやっていた。

Raspberry Pi 5 上の自動売買システムで、データ取得スクリプトの p95 が 2.1s から 2.8s に伸びていた。原因は jq のフィルタを書き換えたことだとアタリは付いていたが、確証が無い。time ./fetch.sh を 5 回回しても、1.9s と 3.4s が混在して結論が出せない。

そこで hyperfine に手を出した。Rust 製のコマンドラインベンチマークツールで、time を 100 回叩いて手で平均を出していた作業を、統計的にちゃんと処理してくれる。今回はその実践的な使い方を、自分が Pi 上でハマったポイントと一緒に書いていく。

hyperfine が解決するもの

time コマンドの問題は、1 回の実行時間しか出ないことに尽きる。ファイルキャッシュの状態、CPU governor の挙動、たまたま走っていた cron ジョブ。これらが噛み合うと、同じスクリプトでも 2 倍の差は普通に出る。

hyperfine は 3 つのことを自動でやってくれる。

  • 指定回数(デフォルト 10 回以上)コマンドを実行して、平均・標準偏差・最小最大を出す
  • 外れ値を検出して警告する(「この結果はノイズが多いかも」と教えてくれる)
  • 複数コマンドを並べて、何倍速いかの比率まで自動計算する

つまり「A と B、どっちが速い?」に対して、エビデンス付きで答えが出せる。time を電卓で平均する地獄から抜けられる。

インストール

Raspberry Pi (Debian/Ubuntu) ならパッケージから入る。

sudo apt install hyperfine

古めの Raspberry Pi OS だとリポジトリに無い場合があるので、その時は GitHub release から arm64 deb を取ってきて dpkg -i。Mac は brew install hyperfine、Arch は pacman -S hyperfine。Rust のクレートとしても配布されているので cargo install hyperfine でも入る。自分は最初これでビルドして Pi 上で 4 分待った。素直に apt にしておけばよかった。

基本的な使い方

単一コマンドを測る

hyperfine './fetch.sh'

これだけで 10 回回して結果を出す。出力はこんな感じ。

Benchmark 1: ./fetch.sh
  Time (mean ± σ):      2.412 s ±  0.087 s    [User: 0.913 s, System: 0.241 s]
  Range (min … max):    2.301 s …  2.583 s    10 runs

標準偏差が出るのが偉い。±0.087s なら、ほぼ 2.4s 前後で安定していると判断できる。これが ±0.5s なら、計測条件に何かある。

2 つのコマンドを比較する

hyperfine 'rg --no-heading "ERROR" logs/*' \
          'grep -r "ERROR" logs/ --include="*.log"'

結果の最後に Summary が出て、どちらが何倍速いかまで出る。自分の Pi 5 上のログ解析だと、ripgrepgrep -r の 7.4 倍速かった。体感ではなく数字で出るのが気持ちいい。

キャッシュの罠を踏まないために

初心者がいちばんハマるのがファイルキャッシュ問題だ。1 回目の実行はディスクから読むので遅く、2 回目以降は OS のページキャッシュに乗って速い。これを放置してベンチを取ると、コマンド A の実行回数が 10 回中 1 回だけディスク読みで、コマンド B はもう全部キャッシュ済み、なんて状況が起きる。

対策は --warmup。指定回数だけ計測前に空回しする。

hyperfine --warmup 3 './fetch.sh'

これで最初の 3 回はキャッシュ温め用に捨てて、4 回目以降を計測する。コールドキャッシュを意図的に測りたい場合は逆に --prepare でキャッシュをドロップさせる。

hyperfine --prepare 'sync && echo 3 | sudo tee /proc/sys/vm/drop_caches' \
          './fetch.sh'

毎回キャッシュを捨ててから測る。これは「初回起動の遅さ」を評価したい時に使う。Pi では drop_caches に root 権限が必要なので、自分は sudoers に NOPASSWD を 1 行足して測定中だけ通している。終わったら戻すのを忘れないこと。

パラメータ掃引でチューニングする

これが本当に強い。同じコマンドを引数違いで何種類も比較できる。

hyperfine --parameter-scan threads 1 8 \
  'cargo build --jobs={threads}'

{threads} が 1, 2, 3, ..., 8 と置換されて全部走る。並列度ごとのビルド時間の表が一発で出る。Pi 5 でやってみると、4 スレッドまでは綺麗にスケールするが、5 スレッド以降はあまり伸びない。コア数 4 だから当然なのだが、可視化されると納得感が違う。

リスト指定もできる。

hyperfine --parameter-list compress 'gzip','zstd','xz' \
  '{compress} -k -f data.json'

3 つの圧縮ツールをまとめて測れる。zstd の速さに毎回驚く。

JSON 出力で CI に組み込む

結果を JSON で吐けるので、CI でリグレッション検出ができる。

hyperfine --warmup 2 --export-json bench.json './fetch.sh'

あとは jq.results[0].mean を抜いて、前回の値と比較すれば閾値超過でアラートが上がる仕組みが作れる。自分は GitHub Actions で main ブランチにマージするたびにベンチを回して、5% 以上の劣化が出たら Slack に投げるようにしている。

current=$(jq -r '.results[0].mean' bench.json)
baseline=$(cat baseline.txt)
ratio=$(echo "scale=3; $current / $baseline" | bc)
if (( $(echo "$ratio > 1.05" | bc -l) )); then
  echo "REGRESSION: ${ratio}x slower"
  exit 1
fi

Pi 上で動かしている自動売買のデータパイプラインにも、この仕組みを入れた。詳しくは Raspberry Pi × Claude Code で作る自動売買システムの本に書いている。CI にベンチを組み込むだけで、「なんか遅くなった気がする」が「3 日前のコミットで p50 が 1.4 倍になった」に化ける。

使う時に気をつけること

min-runs と max-runs の調整

デフォルトは 10 回以上、かつ最低 3 秒は計測する。1 回が長いコマンド(ビルドとか)だと、これだと 30 分かかってしまう。--min-runs 3 で短縮できる。逆にミリ秒級のコマンドは --min-runs 100 くらいに増やしたほうがブレが減る。

シェルのオーバーヘッド

hyperfine は内部で sh -c 経由で実行する。ミリ秒級を測る時はこれが無視できない誤差になる。--shell=none で直接 exec できるが、その場合パイプやリダイレクトは使えない。

外れ値の警告は素直に従う

「Warning: Statistical outliers were detected.」と出たら、何かバックグラウンドで走っている。自分の Pi では cron で動いている systemd timer がたまたま噛み合うことがあった。詳しくは cron から systemd timer への移行の記事で書いた timer の停止手順を使って、計測中だけ止めると安定する。

Pi 上では CPU governor を固定する

Pi 5 はデフォルトで ondemand ガバナーが動いていて、負荷に応じてクロックが変動する。短いベンチだと、最初の数回は低クロックで走って遅い、なんてことがある。performance に固定すると安定する。

echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

計測が終わったら ondemand に戻す。発熱と消費電力が上がるので、つけっぱなしにしないこと。自分はこれを忘れて 1 週間 performance のままにして、ヒートシンクが触れないくらい熱くなって気付いた。

実際に何に使ってきたか

この半年で hyperfine を使った場面を雑に並べてみる。

  • jq のフィルタを .[] | select() から map(select()) に書き換えた時の差分計測。8% 速くなった
  • Python スクリプトの起動時間を uv runpython -m で比較。uv run が 200ms 速い
  • SQLite のインデックスを張る前と後でクエリ速度を測定。1.2 倍程度のはずが 18 倍速くなった(インデックスは正義)
  • Cloudflare Tunnel 経由とローカル直叩きの API レスポンス時間比較。トンネル経由は +12ms 程度の上乗せ
  • Docker コンテナの起動時間を image size 別に計測。alpine ベースが distroless より起動が遅かった(意外)

共通しているのは、「速い気がする / 遅い気がする」を数字に変換できたこと。これがある前と無い前で、議論の質がぜんぜん違う。

同種のツールとの比較

類似ツールには bench(Haskell 製)、multitime、Python の timeit モジュールがあるが、CLI の比較用途では hyperfine が一番手数が少ない。インタラクティブな統計分析が要るなら R や Python に持っていくべきだが、「今この場で比較したい」のニーズには hyperfine で十分。

VPS 上でベンチを回すなら、Pi のような家庭用環境より計測がブレにくい。自分は本番に近い条件で測りたい時はお名前.comの高性能VPSで 1 時間だけインスタンスを立てて、そこに送って測ることもある。VPS は CPU governor が固定されているので、家庭用 Pi より結果が再現しやすい。

導入の最小手順

とりあえず明日から使うなら、この 3 ステップでいい。

  1. sudo apt install hyperfine で入れる
  2. 気になっているコマンドを hyperfine --warmup 3 'コマンド' で 1 回測る
  3. 標準偏差が平均の 5% 以内なら信頼できる数字。それ以上ブレるなら計測環境を疑う

ベンチマーク結果の妥当性については、正直まだ自分も試行錯誤中で、外れ値警告が出た時の判断基準は職人技みがある。それでも、「数字が無いまま議論する」よりは桁違いにマシだ。time でやっていた頃には戻れない。

もしコマンドラインの生産性を上げる工夫を体系的に知りたい場合は、DevTools サイトの方でも関連ツールを紹介している。fzf や ripgrep と組み合わせると、開発体験が一段変わる。