どうもみゅーとんです.
小ネタです.
hacomonoは GitHub の hacomono-lib という organization にて, OSS を公開しているのですが,
なんとなく作っているロジックが多重ループになっていて計算量が多そうだ, と思い, そのパフォーマンス計測を行うことにしました.
概要
3 行でまとめ
- Vitest には標準でベンチマークテストを行う仕組みがある
- ただし experimental (
v1.2.x
時点) - ベンチマークテストには
tinybench
を利用している
ここで話題にしないこと
- 計算量の概念
- パフォーマンス改善のためのコーディング
背景
インシデント未遂を引き起こしてしまいました.
hacomono-lib で公開している OSS の json-origami
にて, パッチバージョンアップとして, たった1行変更するだけのバグ修正を行いました.
この修正が計算量を膨大に引き上げてしまい, プロダクトのコードでパフォーマンスが大幅劣化してしまいました.
暫定対処として, 利用する json-origami
のバージョンを下げるなどの対応はしました.
恒久対処をするためには, 速度が改善されたかどうかを計測する仕組みが前提として必要でした.
実践
Vitest でパフォーマンステストを書く
まず, vitest.config.ts
に以下のような修正を加えます.
今回は test/*
フォルダ配下にある *.bench.ts
ファイルをベンチマークテストを書くファイルとしています.
import { defineConfig } from 'vitest/config' export default defineConfig({ test: { // これを追加 benchmark: { include: ['test/**/*.bench.(js|ts)'] } }, })
ベンチマークテストとして, 以下のようなコードを書きます.
import { bench, describe } from 'vitest' import { createObject } from './test/utils' import { modifyObject } from './src/modifyObject' describe('benchmark test: "modifyObject"', () => { // ベンチマークテスト対象外にしたいコードは外に書く // createObject は例として 500 個値のある json object 作る関数として想定している. const object500 = createObject(500) bench('when use 500 size object', () => { modifyObject(object500) }) })
Vitest の通常のテストと同時に実行できないため, 以下のコマンドを package.json に追加しておくとよいです.
{ "scripts": { "bench": "vitest bench" } }
仕組み
vitest (v1.2時点) では内部で tinybench が使われており, bench 関数の第2 引数の関数を対象に, tinybench でパフォーマンス計測を行っています.
※ experimental なため, 変わる可能性があります.
上記のコードでは省略していますが, bench 関数は第 3 引数で tinybench のオプションを指定できるようになってます. 詳細は以下のとおり.
基本的には, ベンチマークテストしたい最小限のコードを第 2 引数にすれば良さそうです.
上記のコードだと, テストに関係ない “モックデータを作る処理” がbench の外に記載してあることで, 余計な処理をテスト対象に含めないようにしてます.
第 3 引数には time (default 500)
iterations (default 10)
の 2 パラメータがあり, テスト開始から合計 time
ms 経過するまでは何度も実行し計測しますが, この時間を超えて iterations
回数に満たない場合は, 時間超過しつつ必ず iterations
回実施するようになっています.
結果の読み方
以下は json-origami
でパフォーマンステストを実施した結果の一部です
✓ test/twist.bench.ts (6) 1840ms ✓ twist with light object (3) 1836ms name hz min max mean p75 p99 p995 p999 rme samples · twist (complex object including 100 values, twist 10% of keys) 264.30 0.8337 13.3483 3.7835 4.3818 10.4968 13.3483 13.3483 ±7.74% 133 fastest · twist (complex object including 100 values, twist 50% of keys) 215.81 1.3680 10.4523 4.6338 5.3692 9.8662 10.4523 10.4523 ±6.44% 108 · twist (complex object including 100 values, twist 90% of keys) 162.60 2.9027 10.6133 6.1502 6.9685 10.6133 10.6133 10.6133 ±4.95% 82 slowest
ちょっと横に長いですが, それぞれ項目の意味は以下の通り
- hz .. 1 秒間にこの処理を実行できる回数
- min .. 計測したなかで最短だった時間 (ms)
- max .. 計測したなかで最長だった時間 (ms)
- mean .. 標本平均 (ms)
- p75 .. 75 パーセンタイル (ms)
- p99 .. 99 パーセンタイル (ms)
- p995 .. 99.5 パーセンタイル (ms)
- p999 .. 99.9 パーセンタイル (ms)
- rme .. 相対誤差 (%)
- samples .. 標本数
hz?
周波数の Hz と同じ意味合いだと思います. 数が多いほど同じ時間での実行可能数が多いことを意味します.
標本平均?
一般的に平均と呼ばれたりしますが, 統計学上では標本平均と呼ばれたりします.
全標本を足し合わせて, 標本数で割った数です.
パーセンタイル?
小さい方から数えて指定 % の位置にある標本の値を指しています.
ベンチマークテストの結果においては, 例えば p75
であれば, 実行時の 75% はその値以下の時間で処理が完了する意味合いと捉えてよさそうです.
上記の実施結果の例をみると p75
p99
と差が開いていて, 結果が分散している様子が伺えるかと思います. 入力するオブジェクトをテスト毎に変更しているため, 分散する結果になったのかなと思っています.
(後のリファクタリングで安定化しました)
相対誤差?
一般的には (実測値 - 理論値) / 理論値
で表される, 理論値に対する誤差の割合ですが, tinybench においては理論値として標本平均が使われます.
つまり, 平均値に対してどれだけ結果に誤差が生じるかが得られており, いいかえると計測時間がどれだけブレるかがわかります. 少ないほど どんな入力値に対しても近い実行時間が得られる ことがわかります.
標本数?
実行回数です.
指定した time
(ms) の時間内に実行した回数が記載されます.
time
を超えても iterations
回数を実施できなかった場合は, 時間超過して iterations
回数だけは必ず実施します. そのため, 最低値は iterations
と同じです.
高ければ高いほど高い精度の結果が得られます.
まとめ
案外, しっかり理解すると, パフォーマンス改善に役立てそうです.
今回は プロダクトのコードを切り出した OSS でのパフォーマンス検証であるため,
そもそも切り出されたロジックの検証であって, 導入しやすかったかなと思います.
株式会社hacomonoでは一緒に働く仲間を募集しています。
採用情報や採用ウィッシュリストもぜひご覧ください!