motoko-performance-optimizations について
Motoko Performance Optimizationsは、動作を変えずにMotokoのスループットやレイテンシを改善する開発者向けエージェントスキルです。割り当て削減、固定幅演算、バイト処理、テキスト生成、ループ構造と検証方法を扱います。
主な機能
- 01ホットパスの割り当て削減
- 02ビット演算での固定幅算術
- 03Textの大きな単位での構築
- 04均一なブロック処理と末尾処理
- 05テストとベンチマークによる検証
活用シーン
- 01バイトエンコーダーやデコーダーの最適化
- 02ハッシュやバイナリ解析のホットパス改善
- 03Blobの反復処理に伴う割り当て削減
- 04ベースラインと性能変更の比較
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ research-ag/ motoko-skills --skill motoko-performance-optimizationsインストール後、Claude Code で/motoko-performance-optimizationsを入力して読み込まれたか確認します。
詳細
Motoko のパフォーマンス最適化
概要
Motoko コードを安全かつ予測可能に高速化するための拡張可能なガイドです。アロケーションの削減、固定幅演算、ブロック処理、効率的な Text の構築、明確なループ構造など、機械的に適用でき、動作を維持する改善に焦点を当てています。セマンティクスを変えずにスループットやレイテンシを改善したい場合に、このスキルを使用してください。
- ベンチマークの詳細とハーネスについては、こちらを参照してください:skills/benchmarks-generation/SKILL.md
- パフォーマンス改善に先立って行うことの多い、スタイルと安全なリファクタリングについては、こちらを参照してください:skills/code-improvements/SKILL.md
- 簡潔さやオーバーヘッドの改善につながるドット記法については、こちらを参照してください:skills/dot-notation-migration/SKILL.md
すぐにできる改善(全般)
-
ホットパスでのアロケーションを最小限に抑える
- 反復処理のためだけにバッファ全体を実体化しないでください(例:
Blobを直接インデックス指定する方法を優先します)。 - 長さや容量を再利用し、
size()の呼び出し結果はローカル変数にキャッシュします。
- 反復処理のためだけにバッファ全体を実体化しないでください(例:
-
タイトなループでは固定幅演算を優先する
- シフトやマスクには
Nat32/Nat64の中間値を使い、ループの途中でNatに拡張しないでください。
- シフトやマスクには
-
Textは大きなチャンク単位で構築する- 文字ごとに
Text.fromCharを呼び、多数の小さな#連結を行うのは避けてください。ASCII/UTF‑8 のブロックを出力し、ブロックごとに一度だけ連結します。
- 文字ごとに
-
定常状態に適したループ構造にする
- メインループで均一なブロックを処理し、残りの少量のデータは別途処理します。
- 不変値(サイズ、定数など)はループの外に移し、内側のループは直線的な構造に保ちます。
-
変更のたびに検証する
- 再ビルドし、ユニットテスト/プロパティチェックを実行してから、代表的な入力でベンチマークを行います。
- 比較対象となる変更前後の実行では、
[optimize](および固定された[toolchain] wasm-optのバージョン)を同一に保ってください。mops benchはmops.tomlに[optimize]がある場合にのみwasm-optを実行するため、実際にはオプティマイザーの揺らぎ(またはセクションの有無)によるベンチマーク上の「改善」が、コード変更によるものに見えてしまうことがあります。詳しくは skills/benchmarks-generation/SKILL.md を参照してください。
技法の概要
- バイト/ビットのホットパス技法(以下で詳述)
- アロケーションとメモリ管理(近日公開)
Text/文字列の構築パターン(近日公開)- 数値/算術演算と固定幅演算(近日公開)
- データ構造とアルゴリズム(近日公開)
- Async/Await とインターカナスターパターン(近日公開)
- Candid/シリアライゼーションの効率化(近日公開)
- キャッシュとメモ化(近日公開)
関連スキル
- ベンチマーク:skills/benchmarks-generation/SKILL.md
- コード品質の整理:skills/code-improvements/SKILL.md
- ドット記法への移行:skills/dot-notation-migration/SKILL.md
- 一般的なスタイル:skills/motoko-general-style-guidelines/SKILL.md
バイト/ビットのホットパス技法
これらの技法は、Blob または [Nat8] を反復処理するコード、ビットのパッキング/アンパッキングを行うコード、あるいは Text を出力するコード(エンコーダー/デコーダー、チェックサム、バイナリパーサーなど)を対象とします。アロケーションを減らし、高コストな整数の拡張を避け、正確性を保ちながらスループット向上のためにループを再構成します。
使用する場面
- バイトやビット演算を行うホットループがある
- コードが
Textを1文字ずつ、または多数の小さな連結によって構築している Blobが反復処理やインデックス指定のためだけに[Nat8]に変換されている
前提条件
- Motoko コンパイラー(moc)は、ある程度新しいバージョンであること(1.3.0 以降を推奨)
- 固定幅整数型(
Nat8、Nat16、Nat32、Nat64)とラップアラウンド演算(+%)に精通していること - ベンチマークのガイダンスについては、こちらを参照してください:skills/benchmarks-generation/SKILL.md
クイック改善チェックリスト(ヒューリスティック)
候補となるコードを確認し、次のパターンがあれば適切に置き換えます。
-
Blob の処理
- 反復処理やインデックス参照だけを目的として
let bytes = Blob.toArray(data)を使用している場合 →Blobを直接インデックス参照します:data[i]。 data.size()をローカル変数にキャッシュし、適切な幅(多くの場合Nat64)を選びます。
- 反復処理やインデックス参照だけを目的として
-
整数変換とビット演算
- ビット演算のために
Nat8 -> Nat(任意精度)へ拡張するのは避けます。 - 固定幅型を段階的に拡張します。シフトやマスクの前に
b.toNat16().toNat32()のようにします。 <<、>>、&、|はNat32/Nat64の中間値に対して行い、必要に応じて最後に縮小します。- 不変条件によりオーバーフローが起こらない場合は、ラップアラウンド算術
+%を使用します。幅の広いカウンター(例:Nat64)を優先します。
- ビット演算のために
-
アルファベット/ルックアップテーブル
- ホットパスでは
Char/Textのテーブルを避けます。直接バイトを出力できるよう、アルファベットは[Nat8](UTF-8/ASCII コード)として格納します。
- ホットパスでは
-
テキストの構築
- 文字ごとの
Text.fromCharと、繰り返しの#連結は避けます。 - テキストはブロック単位で出力します。小さな ASCII
Blob(例:4~8 バイト)を構築し、Text.decodeUtf8を一度呼び出してから、ブロックごとに一度だけ追加します。 #演算の回数を最小限にします。出力文字 4~8 文字ごとに 1 回追加するほうが、多数回追加するよりはるかに効率的です。
- 文字ごとの
-
ループの形状
- 入力を均一なブロック(例:3 バイト → 4 文字)で処理し、その後、短い末尾(0~2 バイト)を別に処理します。
- 不変条件(サイズ、定数)をループの外に移し、内側のループは直線的に保ちます。
-
正確性のガード
- 理論上到達しない状態(例:ASCII のデコード失敗)には、明確なメッセージを含む
Prim.trap("…")を挿入します。 - ブロック境界でのオフバイワンエラー(例:
next_i <= sz)がないか、念入りに確認します。
- 理論上到達しない状態(例:ASCII のデコード失敗)には、明確なメッセージを含む
変更前 → 変更後のパターン
- Blob から Array への変換を避ける
// Before
let bytes = Blob.toArray(data);
var i = 0;
let b1 = bytes[i];
// After
let sz = Nat64.fromIntWrap(data.size());
var i : Nat64 = 0;
let b1 = data[i.toNat()];
- 固定幅整数と段階的な拡張を優先する
// Before (goes through arbitrary-precision Nat)
let n = (Nat32.fromNat(Nat8.toNat(b1)) << 16)
| (Nat32.fromNat(Nat8.toNat(b2)) << 8)
| Nat32.fromNat(Nat8.toNat(b3));
// After (fixed-width path)
let n = (b1.toNat16().toNat32() << 16)
| (b2.toNat16().toNat32() << 8)
| b3.toNat16().toNat32();
Char/Textのアルファベットと文字ごとの連結を、[Nat8]とブロック単位のデコードに置き換える
// Before
private let alphabet : [Char] = ['A', 'B', /*…*/, '/'];
let c1 = Text.fromChar(alphabet[idx1]);
let c2 = Text.fromChar(alphabet[idx2]);
result #= c1 # c2 # c3 # c4; // many small concats
// After
private let alphabet : [Nat8] = [65, 66, /*…*/, 47]; // ASCII bytes
let bytes = Blob.fromArray([
alphabet[idx1], alphabet[idx2], alphabet[idx3], alphabet[idx4]
]);
switch (Text.decodeUtf8(bytes)) {
case (?t) { result := result # t } // one append per block
case (_) { Prim.trap("Cannot happen: Utf8 decode error …") }
};
- ブロック単位の処理と末尾処理
// Example: main loop over full blocks, then a small tail path
var i : Nat64 = 0;
var next_i : Nat64 = block; // e.g., 3, 6, etc.
while (next_i <= sz) {
// read <block> bytes, produce <k> output chars
i := next_i; next_i +%= block;
};
while (i < sz) {
// read remaining bytes (tail), produce padded output as needed
i +%= tailStep;
};
ステップごとの手順(エージェントのプレイブック)
-
対象範囲の特定
- バイト/ビットを多用するホットパス(エンコーダー/デコーダー、ハッシュ処理、バイナリパーサー)を特定します。
- 必要に応じて、小規模なベンチマークハーネスを用意します(skills/benchmarks-generation/SKILL.md を参照)。
-
ベースライン
- 現実的な入力(連続したバイト、ランダム/混在データ)でベンチマークを実行し、結果を記録します。
-
変換(段階的に適用し、各コミットの対象を絞ります)
- 反復処理やランダムアクセスだけを目的とした
Blob.toArrayを削除し、Blobを直接インデックス参照します。 - サイズをキャッシュし、インデックスに
Nat64を使い、安全な場合は+%を使用します。 Char/Textのアルファベットを[Nat8]のテーブルに置き換えます。- 文字ごとの連結を、
Blob.fromArrayとText.decodeUtf8を使ったブロック単位の出力に変換します。 - シフトやマスクには
Nat16/Nat32/Nat64の中間値を使い、ホットループでのNatへの拡張を避けます。 - ループを、大きく均一なブロックと短い末尾処理に組み替えます。
- 不要な一時変数を削除し、直線的なコードを優先します。
- 論理上到達しないデコードエラーに対して、明示的な
Prim.trapを追加します。
- 反復処理やランダムアクセスだけを目的とした
-
検証
- 単体テスト:短い ASCII の例、パディング/エッジケース、長い連続バイト列を確認し、関連する場合は出力長とアルファベットへの適合性を検証します。
- 任意のプロパティチェック:ランダム入力を参照実装と比較します。
- ベンチマーク:パターンやサイズを変えて、改善または同等の性能を確認します(skills/benchmarks-generation/SKILL.md を参照)。
よくある落とし穴
-
タイトなループ内で不要に
Natへ拡張する- 問題点:任意精度の算術演算は遅く、メモリを割り当てます。
- 対策:中間値には
Nat32/Nat64を使い、Nat8 -> Nat16 -> Nat32の順に段階的に拡張します。
-
文字ごとに
Textを操作する- 問題点:
Text.fromCharと繰り返しの#によって、小さなメモリ割り当てが大量に発生します。 - 対策:ASCII バイトを
Blobブロックに出力し、ブロックごとに一度だけText.decodeUtf8を呼び出してから、一度だけ追加します。
- 問題点:
-
反復処理のために
Blobを[Nat8]として実体化する- 問題点:ホットパスでバッファ全体のメモリ割り当てとコピーが発生します。
- 対策:
Blobに直接インデックスを付け、size()をキャッシュします。
-
ループカウンターのオーバーフローチェック
- 問題点:オーバーフローが確実に発生しないと証明できる場合でも、反復ごとに余分なチェックが行われます。
- 対策:カウンターを
Nat64に拡張し、明確なオーバーフローなしの不変条件のもとで+%を使います。
-
ブロック境界でのオフバイワン
- 症状:入力末尾付近でトラップが発生する、または出力が不正になります。
- 対策:メインループでは
next_i <= szを使い、残りの末尾は明示的に処理します。
安全性とエッジケース
- ラップアラウンド算術:選択したビット幅でオーバーフローしないことが不変条件で保証される場合に限り
+%を使います。Nat64への拡張を優先してください。 Text.decodeUtf8:ASCII バイト(コーデックのアルファベットや=のパディングなど)に対して安全です。監査しやすいよう、防御的なPrim.trapを残してください。- 境界:高速なブロックループと末尾の処理を明確に分けます。ブロックサイズに応じて、サイズ 0、1、2、3、5、6、7 などのエッジケースをテストしてください。
- メモリ割り当ての傾向:バッファの実体化(
Blob.toArray)をなくし、連結を最小限に抑えたことを確認してください。
検証チェックリスト(コピー/貼り付け用)
- テストで、空の入力、最小入力、パディングの境界、連続した長いバイト列、該当する場合は複数文からなる ASCII をカバーしている。
- 出力長の式が成り立つ(コーデックまたはアルゴリズムで定義されているとおり)。
- アルファベットのメンバーシップチェックに合格し、余分な文字がない(エンコーダー/デコーダーの場合)。
- ベンチマークで改善または同等の結果が得られ、混合パターンとゼロパターンの間で性能の後退がない(skills/benchmarks-generation/SKILL.md を参照)。
- ブロック境界でトラップが発生せず、インデックスがオーバーフローしない。
コミットメッセージのテンプレート(任意)
可能な限り、1 つの変更を 1 つのコミットにまとめてください。例:
- エンコーダーで Blob から Array への変換を省略する。
- 不要な Char→Text 変換を避け、
[Nat8]のアルファベットを使う。 data.size()を再利用し、Nat64のインデックスを使う。- ビット演算用に
Nat8をNat16経由でNat32に変換する。 - 入力を均一なブロック単位で処理し、末尾は別途処理する。
- 明示的な不変条件のもとでループインデックスに
+%を使い、インデックスのビット幅を拡張する。 - 多数の小さな
#による連結を、デコード済みブロックの一度の追加に置き換える。 - タイトなループ内の一時変数
c1..c4を削除する。 - 到達不能な
decodeUtf8のエラーパスでトラップする。
効果
これらの手法を適用すると、通常は次の効果が得られます。
- バッファ全体の実体化と文字ごとの
Text操作を避けることで、メモリ割り当てを削減 - 固定幅算術によるビットのパッキング/アンパッキングの高速化
- より大きな
Textチャンクを出力することで、連結のオーバーヘッドを削減 - 高速なブロック処理と末尾処理を明確に分離
- 明示的な不変条件とトラップにより、安全性と監査のしやすさが向上
ブロック暗号/ハッシュ関数のホットパス
このセクションの手法は、MD 形式のブロックハッシュ関数や、同様のブロック構造を持つプリミティブ(RIPEMD-160、SHA-1/2、MD5、Blake、ブロック暗号など)に特化したものです。一般的なアドバイスではありません。ほとんどのコードはこのような形にすべきではありません。
これらの手法が有効な場合
この最適化スタイルが正当化されるには、以下の条件がすべて満たされている必要があります。
- 変換関数で処理される固定サイズの入力ブロック。 アルゴリズムで N バイトのブロック(例:SHA-256/RIPEMD-160 の場合は 64 バイト)が指定され、圧縮関数に繰り返し渡されます。
- 各ブロックに対して多数のラウンドを実行し、少数の固定個数の状態ワードを更新する。 通常、4~8 個の
Nat32/Nat64チェイニング変数を使って 64~160 ラウンドを実行します。ラウンドごとの処理経路におけるタプル/レコードの割り当てが、コストの大半を占めます。 - パブリックなストリーミング API(
write/update+sum/finalize)があり、呼び出し間で部分ブロックをバッファリングする必要がある。 - スループットが重要である。 関数が測定可能なホットパス(例:署名、アドレス導出、大きなメッセージのハッシュ化)にあります。
コードが一度きりの処理である、可変サイズのフレームを処理する、またはリクエストごとに数回しか実行されない場合は、ここでやめてください。代わりに、シンプルで読みやすいコードを使いましょう。参考:src/Ripemd160.mo は、入力が 1 KiB の場合、命令数を約 2 倍、GC を約 3.6 分の 1 にするために書き換えられました。この手法ではインライン化されたラウンドが約 300 行必要になるため、利用側が頻繁に使うプリミティブに限って採用する価値があります。
参考パターン(research-ag/sha2 スタイル)
以下に示すパターンを組み合わせると、research-ag/sha2 の SHA-256 で使われているものと同じ形になり、src/Ripemd160.mo にも適用して効果が得られました。組み合わせて適用してください。一部だけを適用すると、効果の大半を逃してしまいます。
パターン 1 — [var Nat32](または [var Nat64])による可変チェイニング状態
// Persistent chaining state, single allocation reused across all blocks.
private let s : [var Nat32] = VarArray.repeat<Nat32>(0, 5);
チェイニング変数が多数ある場合は、個別の var フィールドを使わないでください。配列スロットには固定幅のワードがインラインで格納されます。測定によって効果が確認できない限り、Nat16 の半分に分割しないでください。状態ワードが 8 個以下の場合、その価値はありません。
パターン 2 — [var Nat32] によるメッセージスケジュールの事前デコード([var Nat8] ではない)
// 16 little-endian (or big-endian) words for the current block.
// Bytes are folded in at write time; transform() reads words directly.
private let msg : [var Nat32] = VarArray.repeat<Nat32>(0, 16);
これは、よく使われる(そして遅い)次のパターンに代わるものです。
// AVOID for hot-path block hashing
private let buf : [var Nat8] = ...; // 64-byte buffer
transform(buf.toArray(), 0); // allocates a 64-byte [Nat8] PER BLOCK
// transform then re-decodes 4 bytes → Nat32 word internally
[var Nat32] の設計では、ブロックごとの toArray() コピーと、transform() 内での読み取り時のデコードの両方をなくせます。
パターン 3 — 部分ブロックの位置を表す、ボックス化されない Nat16 バイトカウンター
// 0..63 byte position within the current block.
// Nat16 is unboxed in mutable storage; Nat is heap-allocated per increment.
private var i_msg : Nat16 = 0;
ホットパスで使う小さな可変カウンターには、Nat16(範囲が収まる場合は Nat8)を使ってください。var x : Nat = 0 は、代入のたびにボックス化された多倍長整数を割り当てます。Nat64 もボックス化されませんが、値が確実に小さい場合にはレジスター幅を無駄にします。
パターン 4 — writeByte でバイトをワードスケジュールにまとめる
private func writeByte(b : Nat8) {
let pos = i_msg;
let wi = Nat16.toNat(pos >> 2);
let lane = pos & 0x3;
let v : Nat32 = Nat32.fromNat16(b.toNat16()) << Nat32.fromNat16(lane << 3);
if (lane == 0) { msg[wi] := v } // first byte: overwrite stale word
else { msg[wi] := msg[wi] | v }; // subsequent bytes: OR in
let next = pos +% 1;
if (next == 64) { transform(); n_blocks +%= 1; i_msg := 0 }
else { i_msg := next };
};
ビッグエンディアンのスケジュール(例:SHA-2)では、レーンのシフト式を調整してください。((3 - lane) << 3)。
パターン 5 — write 内でブロックを高速デコードする
public func write(data : [Nat8]) {
let n = data.size();
if (n == 0) return;
var i = 0;
// (1) Finish any partial block one byte at a time.
while (i_msg != 0 and i < n) { writeByte(data[i]); i += 1 };
// (2) Fast path: decode 16 LE words inline directly from input → msg.
while (i + 64 <= n) {
msg[0] := data[i].toNat16().toNat32()
| (data[i+1].toNat16().toNat32() << 8)
| (data[i+2].toNat16().toNat32() << 16)
| (data[i+3].toNat16().toNat32() << 24);
// ... msg[1] .. msg[15] (15 more identical lines)
transform();
n_blocks +%= 1;
i += 64;
};
// (3) Tail: remaining < 64 bytes go into the partial block.
while (i < n) { writeByte(data[i]); i += 1 };
};
ループは 1 つではなく 3 つです。大きな入力を低コストで処理するのは高速パスです。
パターン 6 — transform() 内のすべてのラウンドをインライン化する
複数ラウンドを実行するプリミティブでは、これが最大の効果をもたらします。元の RIPEMD-160 コードは次のようになっていました。
// AVOID: each call allocates a (Nat32, Nat32) tuple — 320 tuples per block
let (a, c) = r11(a, b, c, d, e, msg, 0, 11);
let (e, b) = r11(e, a, b, c, d, msg, 1, 14);
// ... 158 more
各ラウンドを、ローカルの可変変数を更新するフラットな文の並びにインライン化してください。
var a1 : Nat32 = s[0]; var b1 : Nat32 = s[1]; /* ... */
// Left line round 1: f1(b,c,d) = b ^ c ^ d, K = 0
a1 := rol(a1 +% (b1 ^ c1 ^ d1) +% w0, 11) +% e1; c1 := rol(c1, 10);
e1 := rol(e1 +% (a1 ^ b1 ^ c1) +% w1, 14) +% d1; b1 := rol(b1, 10);
// ... 158 more, with K and rotation amounts varying per round
// Combine back to s without allocating temporaries
let t = s[0];
s[0] := s[1] +% c1 +% d2;
// ...
はい、数百行になります。はい、仕様または元のタプルを返すヘルパーから機械的に書き写したものです。各ラウンドのグループには、その f と K をコメントで記載してください。コンパイラーは現在、これらのヘルパーを自動でインライン化しません。また、呼び出しごとのタプル割り当ては実際に発生します。
パターン 7 — 長さのエンコードで Nat64 から Nat8 への変換をボックス化せずに行う
パディングでは、ビット長を 8 バイトのリトルエンディアンで書き込みます。素朴な Nat8.fromNat(Nat64.toNat(x & 0xff)) は多倍長整数の Nat を経由するため、バイトごとに割り当てが発生します。
// Stage all narrowing on fixed-width types — no Nat allocation.
private func lowByte64(v : Nat64) : Nat8 {
Nat8.fromNat16(Nat16.fromNat32(Nat32.fromNat64(v & 0xff)));
};
パターン 8 — [var Nat8] を割り当てずに writeByte でパディングする
public func sum() : [Nat8] {
let bitlen : Nat64 = ((n_blocks << 6) +% Nat64.fromNat(Nat16.toNat(i_msg))) << 3;
writeByte(0x80);
while (i_msg != 56) { writeByte(0) };
writeByte(lowByte64(bitlen));
writeByte(lowByte64(bitlen >> 8));
// ... 6 more length bytes; the 8th triggers transform()
// serialize state to 20/32 output bytes
};
これにより、一般的な pad : [var Nat8] と sizedesc : [var Nat8] の割り当て、およびそれらの .toArray() コピーをなくせます。
このスタイルを採用する際に取り除くべきアンチパターン
- 1 バイトごとの処理経路で
var counter : Nat = 0を使う →Nat16/Nat64を使います。 - 1 ラウンドごとのヘルパーから複数値のタプル
(Nat32, Nat32)を返す。 transformにバイトを渡すために、ブロックごとにbuf.toArray()を呼び出す。sum/finalizeでpadとsizedescの配列を割り当てる。- ワードを
[var Nat32]に事前デコードしたままにできるのに、各ラウンドのビット演算をCommon.readLE32(arr, i)呼び出しで囲む。 - ホットパスで、バイト単位の変換に
Natを介す。
演算子の優先順位に関する注意
Motoko の ^(XOR)、&、|、+%、<<、>> の優先順位は、C/Rust の感覚と異なる場合があります。f 関数とラウンド式には積極的に括弧を付けてください。
a1 := rol(a1 +% ((b1 & c1) | (^b1 & d1)) +% w0 +% 0x5A827999, 11) +% e1;
確認していない場合は、コンパイラーが ^b1 & d1 を (^b1) & d1 としてグループ化すると決めつけないでください。括弧を追加しても、実行時のコストはかかりません。
検証ワークフロー(必須)
これらの最適化は、誤って書き写しやすいものです(RIPEMD-160 だけでもラウンド処理が 160 行あります)。成功を主張する前に、次の点を確認してください。
- テストスイート全体に合格すること。 ハッシュのテストベクトルは必須です。回転量、K 定数、メッセージワードのインデックスが 1 つでも間違っていると、すべての出力が破損しますが、安定しているように見えるバイト列が生成される場合があります。
- 前のバージョンとベンチマークを比較すること。 大きな入力で命令数が 1.5 倍以上減少せず、GC が 2 倍以上減少しない場合は、バッファリングに問題があるか、ラウンドでまだ割り当てが発生しています。[optimize]
/[toolchain] wasm-opt`(上記のクイックウィンを参照)が両方の実行で同じであることを確認してください。そうしないと、変更ではなくオプティマイザーを測定していることになります。 - 外部のリファレンスがある場合(別の mops パッケージなど)は、それともベンチマークを比較すること。 コストを単に別の場所へ移しているだけではないことを確認できます。
これらのパターンを適用しない場合
- 一度きり、または頻度の低いコード。 リクエストごとに 1 回呼び出す 5 行のハッシュ処理であれば、読みやすいバージョンのままにします。
- ブロック長が可変のアルゴリズム(圧縮、解析など)。
msg : [var Nat32]のスケジュールは、固定位置にある固定幅のワードを前提としています。 - ラウンド数の少ないアルゴリズム(例:
< 16)。インライン化は過剰です。タプルの割り当てコストは、ほかの処理に比べて小さいものです。 - 仕様と照らし合わせて簡単に監査できる状態を保つ必要があるコード。 インライン化されたラウンドは、
for r in roundsループよりも読みづらくなります。新しいプリミティブでは、まず明快なバージョンを書いてリリースし、ベンチマークで効果が確認できた場合にのみインライン化してください。 - ジェネリック/パラメーター化された実装。 このスタイルではアルゴリズムをハードコードするため、
transform()をハッシュのバリアント間で簡単に共有できません。
結果(参考:このリポジトリの RIPEMD-160)
入力サイズ 1024 バイトで測定:
| メトリクス | 変更前 | 変更後 | 高速化 |
|---|---|---|---|
| 命令数 | 1,470,125 | 734,849 | 2.0× |
| GC トラフィック | 160.28 KiB | 43.88 KiB | 3.65× |
| ヒープ(安定時) | 272 B | 272 B | 1.0× |
パブリック API と外部から観察できる動作に変更はありません。24 個すべてのテストファイルが合格します。