motoko-benchmarks-generation について
motoko-benchmarks-generationは、bench-helperを使ってMotokoのベンチマークを書くためのスキルです。各ベンチマークにスキーマとrun関数を定義し、ランナーによる一貫した検出と実行を可能にします。設定方法や測定時の注意点も説明します。
主な機能
- 01スキーマとrun関数でベンチマークを定義
- 02ベンチマークごとに.bench.moモジュールを配置
- 03mo:coreとmo:baseの例を掲載
- 04最適化済みWasmの測定を設定
- 05初期化・変更・計測の落とし穴を回避
活用シーン
- 01Motokoパッケージに再現可能なベンチマークを追加
- 02操作と入力カテゴリごとの性能測定
- 03最適化ベンチマーク実行用にmops.tomlを設定
- 04次元の不一致や入力変更の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ research-ag/ motoko-skills --skill motoko-benchmarks-generationインストール後、Claude Code で/motoko-benchmarks-generationを入力して読み込まれたか確認します。
詳細
bench‑helper による Motoko ベンチマーク
これは何ですか
bench-helper は、ベンチマークの記述方法を標準化する小さな Motoko ライブラリです。
小さなスキーマ(名前、行、列)を使ってベンチマークを定義し、run(row, col)
関数を指定して、バージョン付きのベンチレコードを返します。bench/*.bench.mo 配下の各ファイルで、
1 つのベンチマークモジュールを定義します。これにより、ランナーはすべてのベンチマークを一貫した方法で検出し、実行できます。
前提条件
mops.toml(依存関係とツールチェーンを追加)
すでに mops.toml がある場合は、[dev-dependencies] の下に bench-helper を追加するだけです。
プロジェクトでまだ mo:core ではなく mo:base を使っている場合も、そのまま使用できます。ベンチマーク自体は
mo:base で記述しても、ランタイムのキャニスターには影響しません。
[optimize] が設定されていることを確認してください。 mops.toml に
[optimize] セクションがある場合に限り、mops bench は Binaryen の wasm-opt を実行します。空のセクションでも有効になり(デフォルトの level = "O3" が使われます)、これがないと mops bench は最適化されていない Wasm を計測するため、本番のキャニスターで実際に実行されるものを反映しません。[optimize] を使うには、次の例のように
[toolchain] wasm-opt のバージョンを固定する必要があります。
[optimize]
[toolchain]
wasm-opt = "132" # mops toolchain use wasm-opt <version>
motoko-mops-package-maintenance スキルは、メンテナンス作業中にベンチマークのあるパッケージに対してこの設定を自動で追加します。まだ実行されていない場合は、ベンチマークの数値を信頼する前に自分で追加してください。
ディレクトリとファイルの規約
- リポジトリのルートにある
bench/配下にベンチマークを配置します。 - ファイル名には
.bench.moサフィックスを付け、1 ファイルにつきベンチマークを 1 つ作成します。例:bench/base64.bench.mo。 - 各ベンチマークファイルは Motoko の
module { ... }で、単一のpublic func init() : Bench.V1関数を公開します。 init内でBench.Schemaを構築し、Bench.V1(schema, run)を返します。ここでrun : (rowIndex : Nat, colIndex : Nat) -> ()は、計測対象の処理を実行します。
最小限のスケルトン
import Array "mo:core/Array";
import Bench "mo:bench-helper";
module {
/// One benchmark per file; `mops bench` calls `init()` to obtain the
/// schema and the run function.
public func init() : Bench.V1 {
let schema : Bench.Schema = {
name = "My bench";
description = "What this bench measures";
rows = ["size 16", "size 64", "size 256"]; // input categories
cols = ["operation A", "operation B"]; // operations to measure
};
// Prepare inputs once in `init` so they are not re-created on every call.
// `Array.tabulate` returns an immutable `[Nat8]`; use `VarArray.repeat`
// (or `VarArray.tabulate`) if you need a `[var Nat8]` instead.
let inputs : [[Nat8]] = [
Array.tabulate<Nat8>(16, func _ = 0),
Array.tabulate<Nat8>(64, func _ = 0),
Array.tabulate<Nat8>(256, func _ = 0),
];
// Build a table of routines: routines[ri][ci] : () -> ()
let routines : [[() -> ()]] = Array.tabulate<[() -> ()]>(
schema.rows.size(),
func(ri) {
let input = inputs[ri]; // capture precomputed input
[
func() { ignore input.size() }, // operation A @ inputs[ri]
func() { ignore input[0] }, // operation B @ inputs[ri]
];
},
);
// `Bench.V1` is a *class* (instance type), not a record alias.
// The runner calls the second argument many times; keep it tiny
// and branch-free.
Bench.V1(schema, func(ri : Nat, ci : Nat) = routines[ri][ci]());
};
};
補足:
Schemaのフィールド名はcolumnsではなくcolsです。rows.size()も同様で、schema.rows.size()と記述する必要があります(rowsだけではスコープ内にありません)。mo:core/Arrayにinit関数はありません。不変の[T]を構築するには、Array.tabulate<T>(size, gen)またはArray.repeat<T>(val, size)を使います。可変の[var T]を構築するには、VarArray "mo:core/VarArray"をインポートし、VarArray.tabulate<T>(size, gen)またはVarArray.repeat<T>(val, size)を使います。このような入力には[var T]と注釈を付けてください(たとえば、テーブルの場合は[[var Nat8]])。mo:coreを使わない場合は、mo:coreのインポートをmo:baseに置き換えてください。mo:base/Arrayは不変配列と可変配列の両方を扱い、独立したVarArrayモジュールはありません。 インターフェースはmo:coreと完全には同じではありません。Array API で対応する関数は次のとおりです。mo:coreのArray.tabulate(size, gen)→mo:baseのArray.tabulate(size, gen)mo:coreのArray.repeat(val, size)→mo:baseのArray.freeze(Array.init(size, val))(不変のrepeatに直接対応する関数はありません)mo:coreのVarArray.tabulate(size, gen)→mo:baseのArray.tabulateVar(size, gen)mo:coreのVarArray.repeat(val, size)→mo:baseのArray.init(size, val)(引数の順序が逆です) 完全な対応表については、base-core-migrationスキルを参照してください。
仕組み
- スキーマ
nameとdescriptionでベンチマークを説明します。rowsには、計測するさまざまな操作やバリアントを列挙します(例:「エンコード」、「デコード」)。colsには、さまざまな入力カテゴリを列挙します(例:メッセージサイズ)。
- ランナーとの契約
- バージョン付きレコード
Bench.V1(schema, run)を返します。ランナーはタイミングを記録するため、run(rowIndex, colIndex)を何度も呼び出します。 - デッドコード除去を防ぐため、
run内の副作用や結果は消費してください(例:ignore ...)。
- バージョン付きレコード
よくある落とし穴
- 行と列の不一致
routinesテーブルのサイズがrows.size() x cols.size()になっていることを確認してください。行または列のラベルを追加・削除した場合は、routinesの構築方法も更新してください。更新しないと、一部のセルでは何も実行されません。
run内でコストの高いセットアップを行う- 入力は
initで一度だけ生成し、クロージャに取り込んでください。runではコアの処理だけを行ってください。
- 入力は
- 結果の消費を忘れる
- 戻り値を消費するには
ignoreを使用してください。そうしないと、コンパイラーが呼び出しをデッドコードとして削除する可能性があります。
- 戻り値を消費するには
- 非決定性とタイミングノイズ
runではログ出力、画面への出力、ランダムなメモリ割り当てを行わないようにし、各行でGCの負荷が同程度になるようにしてください。
- 繰り返し実行と変更を伴う処理
bench-helperは、各セルのタイミングサンプルを収集するためにrun(ri, ci)を何度も呼び出します。測定対象の処理がキャプチャした入力を変更する場合(インプレースソートやインプレースのハッシュ更新など)、2回目以降の呼び出しでは、処理後の状態が使われます。インプレースソートの場合は前回の呼び出しですでにソートされた配列が、インプレースのハッシュ更新の場合は前回の呼び出しで残されたアキュムレーターが使われます。run内で入力を再構築する(その割り当てコストも測定対象に含める)か、新たに準備した入力ではなく、変更後の状態で繰り返し実行する処理をベンチマークしていることを説明してください。
その他の例
bench-helper のリファレンスベンチマーク:https://github.com/research-ag/bench-helper/tree/main/bench
動作を確認する
実行環境によって、使用するランナーやコマンドは異なる場合があります。mops.toml に bench-helper を追加し、.bench.mo ファイルを作成したら、次のことを確認してください。
- プロジェクトの依存関係が解決されることを確認します。
mops install - ベンチマークを実行します。
mops bench - 使用するツールチェーン/バージョンに推奨される最新のランナーコマンドについては、
bench-helperパッケージのREADMEを参照してください。