hyperfine （ベンチマーク測定コマンド）の紹介

## この記事を書くにあたって

アプリケーション、パッケージやコマンドラインの検証等でベンチマークを測定する際、
shell組み込みのtimeコマンドを使うことが自分は多い。なお `/usr/bin/time` のGNU time を使うことで実行時間だけでなくCPUや平均・最大使用メモリも測定できるが、今回はその話題に触れない。

私が感じるhyperfineの魅力は、ベンチマークの比較ができること、warmupが指定できることである。それらは以下で解説する。

## hyperfine とは

Rust製のベンチマークコマンドである。installは `cargo` や `brew` などが簡単であろう。
See [Installation](https://github.com/sharkdp/hyperfine?tab=readme-ov-file#installation)

## 主な使い方

timeコマンドとは異なり、ベンチマークの実行対象となるコマンドは `'文字リテラル'` で入れる必要がある。

```sh
# ❌
hyperfine sleep 0.3

# ✅
hyperfine 'sleep 0.3'
```

デフォルトの実行回数は10回だが、`--runs` (or `-r`) で回数を指定できる。

```sh
hyperfine --runs 5 'sleep 0.3'
```

## コマンドの比較

大抵のベンチマークを取る目的は、コードの改善などの比較のためにおこなわれる。hyperfine は二つコマンドを取ると、それぞれの結果と比較の出力する。以下は全く面白みのないベンチマークの比較を記載する。

```sh
hyperfine -r 5 'sleep 0.2' 'sleep 0.3'
```

```d
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個以上も可能である。

```sh
 hyperfine -r 3 'sleep 0.1' 'sleep 0.15' 'sleep 0.2' 'sleep 0.25'
```

```d
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
```

なお、parameterの違いは `-L 変数名 値1,値2,...,値n` のオプションでも実行できる

```sh
hyperfine -r 3 -L sec 0.1,0.15,0.2,0.25 'sleep {sec}'
```

## warmup


さて、hyperfine が time コマンドより便利であると感じたのは、 `--warmup` (or `-w`) コマンドである。ディスクからの読み込みなど I/O の影響がある場合は warmup により（CPUなど部分的に）cacheされるため、I/O 部分の影響を（厳密ではないが）排除できる。

例えば DuckDBの read_parquet にてデータの読み込みについて、--warmup指定無しの場合（おそらく最初の1回目が） Range max の値となり、平均値はそれに引きずられていると思われる。

```sh
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が効いていると推測される。ことが確認できる。

```sh
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` というオプションがある。[公式での例](https://github.com/sharkdp/hyperfine?tab=readme-ov-file#warmup-runs-and-preparation-commands)を参照されたい

## 出力フォーマット

以下の指定が可能である

- `--export-markdown $FILENAME`
- `--export-json $FILENAME`
- `--export-csv $FILENAME`

出力フォーマットは以下のようになる。json形式が個別の測定値まで持ち一番情報量が多い。

結果を可視化するには json形式で出力し、[公式の python script](https://github.com/sharkdp/hyperfine/tree/master/scripts) から `plot_whisker.py` 等をローカルにダウンロードして実行すればよい。ここでも `uv` の威力が発揮する。

```sh
uv run plot_whisker.py bench-out.json
```

### 以下 --export-* の実行結果

#### markdown

```sh
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-markdown /dev/stdout
```

```markdown
| 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

```sh
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-json /dev/stdout
```

```json
{
  "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

```sh
hyperfine -w1 --runs 5 $'duckdb -c "FROM read_parquet(\'lineitem.parquet\');"' --export-csv /dev/stdout
```

```csv
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
```


以上