mcpField
M
motoko-benchmarks-generation

motoko-benchmarks-generation

github.com/research-agテスト・品質059 回インストール9 週間前に更新
リポジトリ

motoko-benchmarks-generation について

motoko-benchmarks-generationは、bench-helperを使ってMotokoのベンチマークを書くためのスキルです。各ベンチマークにスキーマとrun関数を定義し、ランナーによる一貫した検出と実行を可能にします。設定方法や測定時の注意点も説明します。

主な機能

  1. 01スキーマとrun関数でベンチマークを定義
  2. 02ベンチマークごとに.bench.moモジュールを配置
  3. 03mo:coreとmo:baseの例を掲載
  4. 04最適化済みWasmの測定を設定
  5. 05初期化・変更・計測の落とし穴を回避

活用シーン

  1. 01Motokoパッケージに再現可能なベンチマークを追加
  2. 02操作と入力カテゴリごとの性能測定
  3. 03最適化ベンチマーク実行用にmops.tomlを設定
  4. 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 ...)。

よくある落とし穴

  1. 行と列の不一致
    • routines テーブルのサイズが rows.size() x cols.size() になっていることを確認してください。行または列のラベルを追加・削除した場合は、routines の構築方法も更新してください。更新しないと、一部のセルでは何も実行されません。
  2. run 内でコストの高いセットアップを行う
    • 入力は init で一度だけ生成し、クロージャに取り込んでください。run ではコアの処理だけを行ってください。
  3. 結果の消費を忘れる
    • 戻り値を消費するには ignore を使用してください。そうしないと、コンパイラーが呼び出しをデッドコードとして削除する可能性があります。
  4. 非決定性とタイミングノイズ
    • run ではログ出力、画面への出力、ランダムなメモリ割り当てを行わないようにし、各行でGCの負荷が同程度になるようにしてください。
  5. 繰り返し実行と変更を伴う処理
    • 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を参照してください。

コメント