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

速算に注意せよ

概要

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は自動ベクトル化をどう防ぐの?

本当の問題はIEEEの仕様そのものだと思う。確かに、すべての標準は深く掘り下げると悪いところが見えてくるけど、ここでの問題は浮動小数点コードが精度エラーに敏感なことなんだ。仕様に厳密に従うことは精度エラーを解決しないけど、それによってソフトウェアの挙動が決定論的になることは保証される。90%以上の確率で、それが問題を「調整」するものとして無視できるようにしてくれる。でも、精度エラーはバグなんだ。バグの適切な対処法は、バグを修正することであって、決定論を使って無視することじゃない。だけど、それは難しい。設計上の決定や複雑な数学が関わることが多いから(ジンバルロックを考えてみて:それを「修正」するには四元数や他の直交方向空間を理解する必要があって、難しい!)。だから、私たちはそれに対処するしかない。でも、私の意見では --ffast-mathは悪いよりも良いことが多いし、プロジェクトは絶対にそれを有効にすべきだと思う。なぜなら、それが見つける「問題」は、どうせ修正したいバグだから。

自動ベクトル化できないのはIEEE標準のせいじゃなくて、いくつかの操作の順序が関係ないことを表現できないプログラミング言語のせいだ。だから、同時に実行できるかもしれない。ほとんどの人気のあるプログラミング言語には、必要ないところでも逐次的な意味論を強いる欠陥がある。この欠陥がないプログラミング言語もあったけど、たとえばオッカムとか、広まってない。今は計算アプリケーションに興味があるユーザーが少数派だから、この欠陥は主要なプログラミング言語では修正されていないけど、いくつかの言語にはこの効果を達成するための拡張がある、例えばC/C++やフォートランのOpenMP。CUDAもOpenMPに似てるけど、文法は全然違う。浮動小数点演算のIEEE標準は、歴史上最も役立つ標準の一つだ。その理由は、ハードウェア設計者も素人プログラマーも、スピードベンチマークで良い結果を得るために不正をするインセンティブが常にあったから。つまり、ユーザーにはあまり関係ないと思って、結果にエラーを導入することを期待してる。ベンチマークの結果に感動するから。正しい結果が必要なユーザーもいるし、それが命に関わることもある。正確さが重要でない限られた用途、つまり主にグラフィックスやML/AIでは、正確さよりもスピードを優先して設計された専用のアクセラレーター、GPUやNPUを使う方がいい。一般的なCPUにおいて、IEEE標準に完全に準拠していないのは大きな間違いだ。なぜなら、そういう選択の結果はほとんど予測できないから。特に浮動小数点計算の経験がない人が標準を回避しようとする場合、なおさらだ。CUDAやOpenMPなどについて言えば、定義上、いくつかの操作が並列化できるなら、その実行順序は関係ない。順序が重要なら、結果について保証を提供することは不可能だ。どのプラットフォームでも。順序が重要な場合は、プログラマーが必要に応じて並列スレッドの同期でそれを強制する責任がある。ベクトル化されたコードが欲しいなら、C/C++のようなプログラミング言語には頼らず、常にその目的のために開発されたプログラミング言語の拡張、例えばOpenMP、CUDA、OpenCLを使うべきだ。ベクトル化は運任せにしないから。

この記事は、科学ソフトウェアにおける問題の重要性を過大評価してると思う。私が書いた科学コードでは、ノイズプロセスがここで話されているものよりも桁違いに大きいことが多いし、これは多くの(ほとんどの?)現実世界をモデル化したシミュレーションに当てはまると思う(つまり、物理学や化学など)。それに、ファストマスを有効にすると、かなりのパフォーマンス向上(10%以上)が得られることが多い。特に、-fassociative-mathの議論が興味深い。なぜなら、数学的な式をシミュレーションに変換するコードを書く人のほとんどは、どの演算順序が最も正確かを知らないだろうし、単にシミュレーションする方程式の導出をそのままコーディングするから(演算の順序はバラバラになる可能性がある)。だから、このスイッチが結果を変えるなら、シミュレーションしている方程式や、どの順序が最も正しい結果を出すかをじっくり見直すべきだと思う。ただ、ライブラリや特に数学のシミュレーションに関しては、考慮すべきことがかなり違うかもしれないね。

「数学の順序が重要です、これが私がやりたい順序です」っていう文法があったらいいのに。そうすれば、他の数学はすべてファストマスになるけど、注釈があるところだけは別って感じで。

CAD、ロボティクス、今は半導体光学の分野で働いてきたけど、どの分野でも浮動小数点の精度が最後の桁まで大きな問題だったよ。

この問題は、PyTorchを使った深層学習でもApple MPS上で発生してる。多くの操作でデフォルトでファストマスが使われていて、ゴミのような出力が出るんだ。最近、自動回帰型画像生成モデルをトレーニングしているときに遭遇したよ。こちらが同じ問題に遭遇した人たちの議論だよ:https://github.com/pytorch/pytorch/issues/84936

Unum & Posit: https://posithub.org/about

これって皮肉?もしそうじゃないなら、提案されているPosit標準、IEEE P3109についてかな。

なんでこういうアプローチのハードウェアサポートに関する発表がまだないのか、気になるな。

一番恐ろしいのは、実世界の通貨に浮動小数点が使われているのを見ることだ。神様、いろんなことが間違える可能性がある。俺はいつも無符号整数でセントの数を数えてる。もし複数の通貨を扱う必要があれば、ラッパークラスを使うか作るつもりだ。

負の数はどうやって保存するの?

ラッパーは、複数の通貨を扱わない場合でも良いよ。なぜなら、多くの場所で取引がセントの小数点以下の単位で行われるから。用途によっては、小数点を数桁ずらす必要があるかもしれない。必要に応じて、全通貨単位に変換するロジックを持つラッパークラスを常に用意してるし、要件が変わって小数点以下4桁が必要になったりすることもあるから。

最近、これに関して面白い課題に直面してるんだ。LLMの使用コストを計算しようとしてるんだけど、金額がめっちゃ小さいんだよね。Gemini 1.5 Flash 8Bは100万トークンあたり0.0375ドルだって!会計システムを10億分の1ドル単位で動かすべきなのかな?

小数点型を使った方が良くない?

f64を使って実際のお金を取引するシステムを引き継いだんだけど、意外と上手くいってるんだ。エラーやバグもほとんど丸め誤差によるものじゃないし、もしそういうのがあっても簡単に修正できる。だから、セントのために整数を使うっていう「専門家の意見」にいつも驚かされる。これは「Pythonのpickleは危険だから絶対使うな」とか「HTTPは絶対使うな、たとえプログラムがサブネットを出ないとしても」と同じくらいのレベルだよ。

何年も、すべての計算にfloatを使って、丸めていた請求システムを引き継いでいたんだ。JSでも計算して、Pythonのバックエンドでミラーリングしてたから、「Decimalに切り替えればいい」っていうのは簡単な変更じゃなかった…

Excelがすべて浮動小数点で計算していて、IEEE 754を完全には守っていないって知ったら驚くよ。https://learn.microsoft.com/en-us/office/troubleshoot/excel/...(でも、Excelが使われるほとんどのことにはちゃんと機能してるけどね。)

浮動小数点の計算はそんなに怖がることじゃないよ。ルールは基準でしっかり定義されてるし、多くの分野ではパフォーマンスの理由から唯一の現実的な選択肢なんだ。僕はキャリアのほとんどを、数千億ドル分の取引を実行してきた取引システムを書くことに費やしてきたけど、浮動小数点に関連するバグは一度もなかった。固定小数点の計算を使うのは、ほとんどのHFTや科学計算のアプリケーションには全く不適切だよ。

クリプトコミュニティでいつも感心するのは、お金に使われている数値の型をわざわざ聞かなくてもいいところだね。常に8桁の固定小数点だから、浮動小数点の丸め誤差なんてどこにもないんだよ。

C言語を使ってないのが20年近くなるけど、-ffast-mathに対する警告は覚えてる。それは本当に存在すべきじゃない。あれは、-funsafe-math-optimizationsみたいなもののためのスーパーフラグに過ぎないし、後者は本当に危険だってことを明確に示してる(もしかしたら、実は「楽しい」って意味かも!)

記事やコメントの中で見かけなかったことの一つは、feenableexcept()[1]を使ってコード内のNaNの原因を追跡することだね。feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW);を使うと、NaNが出てくるたびにSIGFPEが発生するんだ。もちろん、fast-mathが有効だと機能しないけど、fast-mathを有効にしてないのにNaNが出てくるなら、まずそれを直さないといけないし、見つけるのが難しいこともあるから、feenableexcept()を使うと見つけやすくなるよ。[1] https://linux.die.net/man/3/feenableexcept