世界を動かす技術を、日本語で。

速算に注意せよ

概要

fast-math コンパイラフラグは、浮動小数点演算の高速化を目的としたオプション。 パフォーマンス向上の代償として 正確性や標準準拠 を犠牲にするリスクが存在。 科学計算 など正確な数値が必要な場面では注意が必要。 SIMD最適化 やFTZ(flush-to-zero)など副作用も多岐にわたる。 安全に使うには、 十分な検証と限定的な適用 が必須。

fast-mathとは何か

  • fast-math は、GCCやClangの-ffast-math、ICCの-fp-model=fast、MSVCの/fp:fast、Juliaの--math-mode=fast@fastmathなど、複数の言語・コンパイラで利用可能なコンパイラフラグ。
  • 浮動小数点演算を 高速化 するため、IEEE 754標準の一部ルールを 無視 して最適化を行う。
  • 正確性 を犠牲にしてでも 処理速度 を重視する設計思想。

fast-mathが有効にする主なオプション

  • -fno-math-errno-funsafe-math-optimizations-ffinite-math-only-fno-rounding-math-fno-signaling-nans-fcx-limited-range-fexcess-precision=fastなどが有効化。
  • -funsafe-math-optimizationsはさらに複数の細かい最適化(-fno-signed-zeros-fno-trapping-math-fassociative-math-freciprocal-mathなど)を内包。

問題になりやすい主な最適化

  • -ffinite-math-only
    • NaNやInfが存在しない 前提で最適化を行う。
    • isnanチェックなどが 自動的に削除 され、異常値の検出や処理が機能しなくなる危険性。
  • -fassociative-math
    • 演算の 順序変更 (再結合)を許可。
    • 浮動小数点演算では丸め誤差が異なり、結果が大きく変わる場合がある。
    • 例:(a + b) + ca + (b + c)で異なる結果。
  • ベクトル化(SIMD最適化)
    • SIMD命令による高速化のため、 演算順序の変更 が行われる。
    • Kahanサミュレーション等、 順序依存アルゴリズム では誤差補正が効かなくなり、精度低下やバグの原因。
  • サブノーマル数のゼロ化(FTZ, DAZ)
    • 極小値(サブノーマル数)を ゼロに変換 し、性能劣化を防止。
    • 一部の数値解析アルゴリズムや理論的性質(Sterbenzの補題など)を 破壊 し、収束失敗などの問題を引き起こす。
    • スレッド単位でFPU制御レジスタが変更されるため、 共有ライブラリのロードだけで副作用 が波及。

fast-math利用時の注意点

  • 科学計算や金融計算 など、 数値の正確性が最重要 な分野では基本的に非推奨。
  • オーディオ、グラフィックス、ゲーム、機械学習 など、多少の誤差が許容される分野では有用な場合も。
  • しかし、 予期せぬバグや再現性のない問題 が起きやすく、十分な理解と検証が不可欠。

安全なfast-math活用手順

  • 信頼できるバリデーションテスト の作成。
  • ベンチマーク による性能評価。
  • fast-mathを有効化し、 結果を比較・検証
  • 最適化オプションごと に有効/無効を切り替え、 影響範囲を特定
  • 最終的な数値結果の 妥当性検証
  • 必要最小限の範囲・設定 でのみfast-mathを適用。

まとめと提言

  • fast-mathは強力だが危険な両刃の剣
  • パフォーマンス改善を狙う場合も、 副作用や数値的な影響を十分に理解 した上で、 限定的かつ慎重に運用 することが重要。
  • 可能であれば、 数値検証やユニットテストの自動化 を取り入れ、 本番環境への適用前に徹底的な検証 を行うべき。

Hackerたちの意見

以前に話題になったのは、https://news.ycombinator.com/item?id=29201473 です(この記事の最後にもリンクされています)。

Forthには固定点の哲学があります。https://www.forth.com/starting-forth/5-fixed-point-arithmeti... 32ビットや64ビットの数値では、小数をそのままスケールアップできます。だから、トーバルズは正しかった。危険なコンテキスト(超精密な医療用投与量など)では、FPには存在する理由があって、私も完全には確信が持てません。また、ForthとLispの両方では、浮動小数点数の前に表現された有理数を使うことを内部的に推奨しています。https://t3x.org のおもちゃのLispでも有理数があります。Schemeでは、exact->inexactとinexact->exactの両方があって、有理数をFPに変換したり、その逆もできます。LinuxやBSDのディストリビューションを使っているなら、依存関係としてGuileがすでにインストールされているかもしれません。だから、実行してみて: scheme@(guile-user)> (inexact->exact 2.5) $2 = 5/2 scheme@(guile-user)> (exact->inexact (/ 5 2)) $3 = 2.5 こうして、Forthでは、有理数用のq{+,-,,/}操作の良いセットを持っていて(カスタムコーディングで、たったの4行)、99%のケースでうまく機能します。無理数については、NASAが16桁を使い切ったし、古い113/355は地球で作られた99.99%の部品には十分な精度があります。天文学的な距離にはあまり向かないかもしれないけど、まあ… Schemeでは: scheme@(guile-user)> (exact->inexact (/ 355 113)) $5 = 3.1415929203539825 Forthでは、pi 355 133 m*/ ;を使うだけで、ほとんどの測定対象に対して高い精度が得られます。

Rustで「代数演算」のAPIを設計するのを手伝ったよ。https://github.com/rust-lang/rust/issues/136469 で、順調に進んでる。これらの演算は、1. ローカライズされていて、関数全体やプログラム全体のフラグではない。2. 完全に安全で、-ffast-mathはNaNがないという仮定を含んでいて、それを破ると未定義の動作になる。じゃあ、これらの代数演算は何をするの?まあ、一つだけでは普通の演算と比べてあまり意味がないけど、一連の演算は代数的に正当化された最適化を使って変換できるから、まるですべての演算が実数演算で行われるかのように。

これらの呼び出しは、x86のMXCSRでFTZとDAZフラグをクリアするの?ARMのFPCRでFZとFIZはどう?

それって、これらの演算を使って書かれた物理エンジンが、異なるプラットフォームで常に同じ決定論的な結果を生成することを意味するの?(それらが代数演算を正しく実装している、またはできると仮定して)

それ、面白そうだね。本当に面白いのは、言語がプログラマーが手動でやるのが面倒なことを自動化して、結果的に生じる丸め誤差の影響を明らかにする手助けをしてくれることだと思う。例えば、逆の丸め方向で2回実行したり、内部的にランダムな方向で何度も実行したり(*のセクション4にある2つのオプション)。つまり、Rustが浮動小数点の微妙なところを隠すんじゃなくて、学べるようにしてくれるといいな。 * https://people.eecs.berkeley.edu/~wkahan/Mindless.pdf

-ffast-mathは実際には15個くらいの別々のフラグみたいなもので、必要なら個別に使えるよ。その中の3つは「NaNなし」、「無限なし」、「サブノーマルなし」。他のいくつかのフラグは、数学を結合的または分配的に扱うことを許可するものもある。ライブラリにはいくつかの利点があるけど、ここで述べた目標は5つのコンパイラフラグで達成できる。ライブラリの利点は、これらがいつ適用されるかを選べることだ。

-funsafe-math-optimizations なんで楽しくて安全な数学の最適化がダメなの?!(笑)

あは!それを見たとき、毎回「楽しくて安全」って読んじゃうってコメントしようとしてた。コンパイラのフラグを毎日扱ってないとそうなるのかな。

「このジェットコースターは、楽しさと安全性を最適化してるよ!」

本当の問題はIEEEの仕様そのものだと思う。これには、個別には99.9%の浮動小数点コードには関係ないような制約がたくさん含まれていて、全体としても野生のコードセグメントの大多数には関係ないものばかり。重要じゃないってわけじゃないけど、これらの機能のいくつかはオプトインにすべきだったと思う。少なくとも、基準は今日のハードウェアの現実をサポートするように進化する必要がある。オートベクトル化できないのは、数十年続いているハードウェアのトレンドを考えるとかなり重大なバグのように思える。一方で、プラットフォームに依存しない決定論を犠牲にするのも簡単なコストじゃない。OpenCLやCUDAの詳細には詳しくないけど、特定の演算順序を保証して、コードがすべてのプラットフォームで予測可能な結果を持ち、なおかつGPUでうまく並列化できる方法があるのかな?

IEEE 754は自動ベクトル化をどう防ぐの?

Hacker Newsで議論の続きを見る