
2026/01/07 20:39
Apple Silicon 上の CPU カウンタ: 記事 + ツール
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Apple の新しい「Lauka」ツールは、M1–M4 チップ上のパフォーマンスモニタリングユニット(PMU)を調査できるようにし、利用可能なすべてのカウンタとその設定を明らかにします。リバースエンジニアリングされた
kperf フレームワーク(kpep_event、kpep_db)をベースに構築され、Lauka はイベント名・マスク・別名を一覧表示し、それらが固定カウンタかどうかを示します。固定カウンタは「Cycles」と「Instructions」の 2 つです。
コマンドラインインターフェイスでは、最大 10 個のカウンタ(カウンタマスクフィールドは 10 ビット幅)を選択でき、サンプリング前にウォームアップステップが含まれます。Lauka は競合を検出します:一部のカウンタグループは同時に実行できず、追加順序が重要です。たとえば、Group M カウンタは同じマスク(
0b0010000000)を共有し、ペアワイズで互いに非互換です;Group G カウンタは 4 個以上選択すると相互排他になります。カウンタの順序を入れ替える(例:ST_UNIT_UOP と INST_LDST をスワップ)ことでこれらの競合が解消され、将来のツールで順序処理を自動化できる可能性を示唆しています。
また、ツールはスロット割り当てルールにも従います:広いマスクは最低ビットから開始して複数のスロットを占有し、最初に追加すると「特殊」カウンタをブロックすることがあります。Lauka の軽量設計は、既存の macOS インストゥルメントと比べて測定時間を約 78 % 削減します(ベンチマーク参照)。
現在、Lauka は Apple Silicon のみをサポートしていますが、将来的には Linux への拡張や根本原因分析の深化が期待されます。PMU データを Apple Silicon 上で利用可能にすることで、本プロジェクトはクロスプラットフォームのパフォーマンスツール開発を促進し、これらのチップ向けコード最適化手法に影響を与える可能性があります。
本文
以前、Apple Silicon での Zig におけるプロファイリングについて書いた際に、PMU カウンタを利用した手法に触れました。
今回はさらに踏み込み、M1・M2 以降の Apple Silicon プロセッサが持つすべてのカウンタを取得できる独自ツールを作成することにしました。
PMU カウンタの簡単な説明
PMU(Performance Monitoring Unit)カウンタは、CPU 内で発生するマイクロアーキテクチャイベントを追跡するハードウェアイベントカウンタです。
例としては、実行された命令数・退避した操作数・分岐回数・キャッシュミス数などがあります。
CPU は通常、固定カウンタとプログラマブルカウンタの両方を公開します。
- 固定カウンタ:あらかじめ定義されたイベント(多くはサイクル数や命令数)を追跡します。
- プログラマブルカウンタ:任意のイベントセットに設定できます。
PMU カウンタを利用することで、開発者はアプリケーションの性能特性(キャッシュミス数・分岐誤判定数・命令混合比など)をより詳細に把握できるようになります。
動機
これらカウンタを取得する手段として、Andrew Kelly が作成した poop ツールと、tensorush によって Apple Silicon で CPU カウンタを取得できる PR を追加したものがあります。
しかし Andrew によりこの PR は丁寧に却下されました。実装が増えるほどサポートが難しくなるためです。
そこで私はフォークを作成し、必要なときに複数の事前定義済み PMU カウンタを取得できるソリューションとして提供します。
しかしさらに進めて Apple Silicon Mac で利用可能なすべてのカウンタを取得できる別ツールを実装することにしました。その過程で Apple の非公開 kperf API を理解し、研究プロジェクトへと発展させました。
この記事はその研究の旅路です。
実験環境
M2 Pro 搭載 MacBook(macOS 15.6.1)を使用しました。
Apple Instruments と奇妙な制限
- Instruments → 「CPU Counters」テンプレートを開き、カウンタを追加/削除しようと試みました。
- 10 個以上のカウンタは取得できず(場合によっては 8 個程度)、
が既に追加されたイベントと衝突するといったエラーが頻繁に発生しました。<SOME_COUNTER> - 最大で取得可能なのは 10 個です。
結論:取得できるカウンタ数には上限があり、ある種のカウンタは互いに不整合があります。
10 と 8 の違いは、2 つのカウンタが固定(CPU が常に監視)であることから説明できます。 Instruments 上では特別なエイリアスを持ちます。
以下は利用可能な 60 個のうち 5 個だけを抜粋した表です。
| カウンタ | エイリアス | マスク |
|---|---|---|
| INST_ALL | – | 0b0010000000 |
| INST_BARRIER | – | 0b0011100000 |
| Cycles (FIXED_CYCLES) | Cycles | 0b0000000001 |
| L1D_CACHE_MISS_LD | – | 0b0011100000 |
| Instructions (FIXED_INSTRUCTIONS) | Instructions | 0b0000000010 |
Cycles と Instructions は固定カウンタで、
FIXED_ プレフィックスとエイリアスを持ちます。 Instruments 上では 2 つだけです → Apple Silicon には 固定カウンタが 2 個 存在すると仮定できます。
kperf の逆解析と最初の実験
次に参照したコードは、macOS の非公開フレームワーク kperf を利用して CPU カウンタを取得する逆解析済みコードです(ibireme による)。
公式ドキュメントは存在しないため、逆解析コードを読むか自ら逆解析するしかありません。ツールは
sudo 権限が必要です。
Zig 版の kperf コードを利用して最初の実験として、すべてのカウンタペアを列挙し、監視リストに追加して単純関数でカウンタ値を取得しました。
結果:グループ M に属する 6 個のカウンタがペアで不整合(同時に追加できない)ことが判明。
| グループ M (6 カウンタ) |
|---|
| INST_ALL |
| INST_INT_ALU |
| INST_INT_ST |
| INST_LDST |
| INST_SIMD_ALU |
| RETIRE_UOP |
8 個の互いに不整合でないように見えるカウンタを追加しようとしたが、エラー「cannot be added together」が発生。ペア以外にも何か別の制約があることが分かりました。
さらに実験を続け、3, 4 個…最大 8 個まで全組み合わせを生成して調べました。
組合せ解析で分かったこと
| 集合サイズ | 発見結果 |
|---|---|
| ペア | グループ M の 6 カウンタのみ |
| トリプレット | 新たな不整合はなし(グループ M を除く) |
| クワッドレプト | グループ G (18 カウンタ) が追加で発見。グループ M と組み合わせると 3 個までしか選べない。 |
| 5–6 個 | 新たな不整合はなし(既知のものを除く) |
| 7 個 | 多数の新規不整合 が出現 |
グループ G (18 カウンタ) は以下です。
BRANCH_CALL_INDIR_MISPRED_NONSPEC BRANCH_COND_MISPRED_NONSPEC BRANCH_INDIR_MISPRED_NONSPEC BRANCH_MISPRED_NONSPEC BRANCH_RET_INDIR_MISPRED_NONSPEC INST_BARRIER INST_BRANCH INST_BRANCH_CALL INST_BRANCH_COND INST_BRANCH_INDIR INST_BRANCH_RET INST_BRANCH_TAKEN INST_INT_LD INST_SIMD_LD INST_SIMD_ST L1D_CACHE_MISS_LD_NONSPEC L1D_CACHE_MISS_ST_NONSPEC L1D_TLB_MISS_NONSPEC
最終的に 18 673 166 個の 7 要素集合で不整合が発生。
C(55, 7) = 202 927 725 通りの組み合わせを調べ、Apple のガイドと実際のカウンタ数とのずれから 60 個中 55 個だけを検証しました。
順序が重要
最後に気づいたことは 追加順序が結果に影響する という点です。
以下の順序で追加するとエラー(赤枠)になります。
L1D_TLB_ACCESS L1D_TLB_MISS L1D_CACHE_MISS_ST L1D_CACHE_MISS_LD LD_UNIT_UOP ST_UNIT_UOP INST_LDST ← error (red circle)
逆に最後の 2 個を入れ替えるとエラーは解消します。
L1D_TLB_ACCESS L1D_TLB_MISS L1D_CACHE_MISS_ST L1D_CACHE_MISS_LD LD_UNIT_UOP INST_LDST ST_UNIT_UOP ← works fine
したがって、Apple Instruments で不整合エラーが出た場合はカウンタの順序を変えてみると解決する可能性があります。
kpep_event と kpep_db の重要性
逆解析コードには次の構造体が登場します。
typedef struct kpep_event { const char *name; // イベント名(例:"INST_RETIRED.ANY") const char *description; // 説明 const char *errata; // エラー情報(NULL が多い) const char *alias; // 別名(例 "Instructions", "Cycles") const char *fallback; // 固定カウンタ用フォールバックイベント名 u32 mask; u8 number; u8 umask; u8 reserved; u8 is_fixed; } kpep_event; typedef struct kpep_db { const char *name; // データベース名(例 "haswell") const char *cpu_id; // plist 名(例 "cpu_7_8_10b282dc") const char *marketing_name; // マーケティング名 void *plist_data; void *event_map; // イベントマップ(CFDict<CFSTR(event_name), kpep_event *>) kpep_event *event_arr; // すべてのイベント構造体配列 kpep_event **fixed_event_arr; // 固定カウンタ配列 void *alias_map; usize reserved_1, reserved_2, reserved_3; usize event_count; usize alias_count; usize fixed_counter_count; usize config_counter_count; usize power_counter_count; u32 architecture; u32 fixed_counter_bits; u32 config_counter_bits; u32 power_counter_bits; } kpep_db;
kpep_event の mask フィールドが鍵です。例えば、以下のように表示します。
| # | Name | Alias | Mask |
|---|---|---|---|
| 1 | FIXED_CYCLES | Cycles | 0b0000000001 |
| 2 | FIXED_INSTRUCTIONS | Instructions | 0b0000000010 |
| … | … | – | 0b1111111100 / 0b0010000000 など |
アルゴリズムの解説
- 最大カウンタ数 = 10(マスク幅 10 ビット)。
- 固定カウンタ(Cycles・Instructions)はユニークで、他のカウンタと互換性があります。
- グループ M の 6 カウンタはペアで不整合。全て同じマスク (0b0010000000) を持ち、同一スロットを競合します。
- グループ G の 19 個のカウンタも同様に特定マスクを共有し、グループ M と重複します。3 個までなら各スロットが空きますが、4 番目以降はスロット不足で失敗します。
順序が重要 な理由:カウンタ追加時に、マスクの低位ビットから最初に利用可能なスロットを割り当てるためです。
広いマスクを持つイベントを先に追加すると、そのスロットを占有し、後で「特殊」なマスクを持つカウンタが必要になったときに衝突します。従って、マスクの昇順で追加する と予測可能です。
Instruments でエラーになる例
| ステップ | カウンタ | マスク | 結果(✓=free, 🔴=conflict) |
|---|---|---|---|
| 0 | 初期状態 | – | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 |
| 1 | L1D_TLB_ACCESS | 1111111100 | 🟢🟢🟢🟢🟢🟢🟢🟡🟢🟢 |
| 2 | L1D_TLB_MISS | 1111111100 | 🟢🟢🟢🟢🟢🟢🡇🟡🟢🟢 |
| … | … | … | … |
| 6 | ST_UNIT_UOP | 1111111100 | 🟢🟢🟡🟡🟡🟡🟡🟡🟢🟢 |
| 7 | INST_LDST | 0010000000 | 🔴 (conflict) |
最後の二つを入れ替えると:
5 LD_UNIT_UOP 6 INST_LDST 7 ST_UNIT_UOP
すべてが空きで衝突は発生しません。
まとめ
私は “Lauka” というツールを作成しました。
- 元の poop リポジトリと scoop ライブラリをベースに構築。
- CLI を書き直し、イベント選択・全カウンタ表示・ウォームアップ機能など拡張。
- Linux と Intel のサポートは除外 – Apple Silicon Mac でのみ動作します。
GitHub の README に完全な使用方法を記載していますのでご覧ください。
サンプル実行
$ lauka -- "./build-old" './build-new -O2' Benchmark 1 (9 runs): ./build-old measurement mean ± σ min … max outliers wall_time 591ms ± 7.6ms 583ms … 605ms 0 (0%) peak_rss 137MB ± 0.3MB 136.6MB … 137.4MB 0 (0%) core_active_cycle 2.51G ± 22.1M 2.48G … 2.54G 0 (0%) inst_all 3.62G ± 23.9M 3.53G … 3.69G 0 (0%) l1d_cache_miss_ld_nonspec 3.58M ± 31.7K 3.54M … 3.63M 0 (0%) branch_mispred_nonspec 21.4M ± 58.2K 21.3M … 21.5M 0 (0%) Benchmark 2 (9 runs): ./build-new -O2 measurement mean ± σ min … max outliers delta wall_time 130ms ± 8.3ms 125ms … 141ms 0 (0%) ⚡ −78.0% ± 0.5% peak_rss 91.9MB ± 0.09MB 91.8MB … 92.1MB 0 (0%) −32.9% ± 0.1% core_active_cycle 507M ± 2.35M 503M … 511M 0 (0%) −79.8% ± 0.1% inst_all 796M ± 10.7M 781M … 809M 0 (0%) −78.0% ± 0.1% l1d_cache_miss_ld_nonspec 352K ± 7.7K 318K … 355K 0 (0%) −90.2% ± 0.1% branch_mispred_nonspec 4.52M ± 11.5K 4.51M … 4.57M 2 (5%) −78.9% ± 0.0% Summary
最後に
- Apple Silicon のみを対象に検索 していたので、Linux を先に調べていればもっと情報が得られたかもしれません。
- 逆解析コードはざっと見ただけで十分だったのですが、早めに深掘りすれば時間を節約できました。
- 最終的な不整合数に時間を費やし過ぎて、本質的な原因解明が遅れた点は残念です。
それでもこの経験を通じて得られた知見は貴重で、共有できて嬉しいです。ご読了ありがとうございました!