1 ポイント 投稿者 GN⁺ 2024-07-05 | 1件のコメント | WhatsAppで共有

幸せな分岐予測器をからかってはいけない

  • 最近 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

原因分析

  • 分岐予測器が blret の組み合わせの不一致で混乱した
  • ARM の文書によると、ret 命令は関数の戻りを予測するのに役立つ

解決方法

  • ret の代わりに br x30 を使用
  • 性能回復: 913 ns

追加最適化

  • foo をインライン化して性能向上
  • ループアンローリングと SIMD 命令の使用

最終性能

  • SIMD + 手動ループアンローリング: 94 ns

結論

  • 分岐予測器を混乱させないこと
  • SIMD コードのほうが速いが、浮動小数点加算は結合法則に従わないため結果が異なる可能性がある

GN⁺の意見

  • この記事は AArch64 アセンブリ最適化の重要性をよく示している
  • 分岐予測器の動作原理を理解することが性能最適化に不可欠である
  • SIMD 命令を使った最適化は非常に効果的だが、精度の問題を考慮する必要がある
  • Rust のような高水準言語を使えば、コンパイラ最適化によって性能を容易に向上させられる
  • 類似の機能を持つプロジェクトとして Agner Fog のアセンブリ最適化ガイドがある

1件のコメント

 
GN⁺ 2024-07-05
Hacker Newsの意見
  • Apple II時代の友人たちと一緒に記事を要約していた

    • 最適化されたコードは、1024個の32ビット浮動小数点数を合計するのに94ナノ秒かかる
    • 1 MHzの6502は、94ナノ秒の間に最初の命令の最初のバイトをメモリから取りに行こうとする程度だろう
    • このコードはキャッシュ上で実行されるときにのみ最適化された性能を発揮する。DRAMは遅い
  • Raymond Chenはほぼ20年前に同じテーマを扱っていた

    • ループ終了を確認した後、分岐なしでfoo関数へ進む
    • 基本的な予測ヒューリスティックに反している
    • 分岐予測器がリターンアドレスのシャドウスタックを維持するのは、数十年前から存在している
  • SIMDコードでは、浮動小数点加算は結合法則に従わないため、異なる順序で合計が行われることがある

    • これがコンパイラがSIMD命令を生成しない理由かもしれない
    • 浮動小数点の合計には本質的に誤差範囲があり、その範囲内のあらゆる答えは有効である
    • 特殊な浮動小数点入力がある場合、言語はそれを明示的にエンコードできる手段を提供すべきだ
  • Rust 1.78以降、コンパイラはより積極的なループアンローリングと多少のSIMDを使う

    • ループアンローリングはRust 1.59で始まった
    • GitHubのコードではRust 1.67.0-nightly版を使っていた
  • ARM/ARM64アセンブリでx0がどのように増加するのか混乱した

    • ldr s1, [x0], #4 命令は、x0を4増やしながらロードする
    • x86_64には、ロードと加算を同時に行う単一命令はない
  • アセンブリコードを最適化するために、もっと複雑でない方法を試していないのは驚きだった

    • ループの末尾で1つの分岐だけが必要になるよう、アセンブリコードを書き直せる
    • fooをインライン化してRET命令を省略できる
  • 筆者が単位を何度も切り替えなければよかった、という意見があった