幸せな分岐予測器をからかってはいけない
- 最近 AArch64 アセンブリをたくさん書いている
- ループ内のジャンプを1つ減らそうという「賢い」アイデアが性能を低下させた
- このミスを説明して、他の人が同じ失敗をしないようにする
コード例
float run(const float* data, size_t n) {
float g = 0.0;
while (n) {
n--;
const float f = *data++;
foo(f, &g);
}
return g;
}
static void foo(float f, float* g) {
// g를 수정하는 작업
}
AArch64 アセンブリへの変換
// x0: const float* data
// x1: size_t n
// s0: 반환할 float
stp x29, x30, [sp, #-16]!
mov s0, #0.0
loop:
cmp x1, #0
b.eq exit
sub x1, x1, #1
ldr s1, [x0], #4
bl foo
b loop
foo:
// s1에서 읽고 s0에 누적
// ...
ret
exit:
ldp x29, x30, [sp], #16
ret
最適化の試み
bl 命令を減らして性能を向上させようとした
- しかし性能はかえって低下した
性能比較
- 元のコード: 969 ns
- 最適化コード: 3.85 µs
原因分析
- 分岐予測器が
bl と ret の組み合わせの不一致で混乱した
- ARM の文書によると、
ret 命令は関数の戻りを予測するのに役立つ
解決方法
ret の代わりに br x30 を使用
- 性能回復: 913 ns
追加最適化
foo をインライン化して性能向上
- ループアンローリングと SIMD 命令の使用
最終性能
- SIMD + 手動ループアンローリング: 94 ns
結論
- 分岐予測器を混乱させないこと
- SIMD コードのほうが速いが、浮動小数点加算は結合法則に従わないため結果が異なる可能性がある
GN⁺の意見
- この記事は AArch64 アセンブリ最適化の重要性をよく示している
- 分岐予測器の動作原理を理解することが性能最適化に不可欠である
- SIMD 命令を使った最適化は非常に効果的だが、精度の問題を考慮する必要がある
- Rust のような高水準言語を使えば、コンパイラ最適化によって性能を容易に向上させられる
- 類似の機能を持つプロジェクトとして Agner Fog のアセンブリ最適化ガイドがある
1件のコメント
Hacker Newsの意見
Apple II時代の友人たちと一緒に記事を要約していた
Raymond Chenはほぼ20年前に同じテーマを扱っていた
SIMDコードでは、浮動小数点加算は結合法則に従わないため、異なる順序で合計が行われることがある
Rust 1.78以降、コンパイラはより積極的なループアンローリングと多少のSIMDを使う
ARM/ARM64アセンブリでx0がどのように増加するのか混乱した
ldr s1, [x0], #4命令は、x0を4増やしながらロードするアセンブリコードを最適化するために、もっと複雑でない方法を試していないのは驚きだった
筆者が単位を何度も切り替えなければよかった、という意見があった