メインコンテンツへスキップ
#82653likely_accept

math: add FMA32

新規提案

要約

AIによる要約であり、誤りを含む場合があります。

概要

math パッケージに単精度(float32)版の融合積和演算(FMA: fused multiply-add)math.FMA32 を追加する提案です。FMA は x*y + z を途中で丸めずに計算し、最後に1回だけ丸める演算です。float64 版の math.FMA はすでにあり、今回はその float32 版にあたります。

ステータス変更

(新規) → likely_accept
2026年10月29日、@aclements が proposal review group を代表して likely accept を表明しました。次の点が評価されたと考えられます。

  • float32 で正しく1回だけ丸める FMA を、ライブラリ側で速く書く方法がない
  • コンパイラによる組み込み関数化(intrinsify)は math パッケージの中でしかできない
  • 具体的な利用者として SIMD パッケージがある
  • 純 Go のフォールバック実装がすでにあり、Berkeley TestFloat-3e(math.FMA の検証にも使ったテストスイート)にも合格している
    提案者の @aclements 自身が「Issue 作成から短期間で」likely accept に進めています。反対意見は出ていません。

技術的背景

現状の問題点

すぐ思いつくのは次の書き方ですが、結果が正しくなりません。

r := float32(math.FMA(float64(x), float64(y), float64(z)))

この式では丸めが2回起きます(二重丸め)。1回目は math.FMA が float64 に丸めるとき、2回目は float64 から float32 に変換するときです。結果がちょうど2つの float32 の中間に来た場合などに、1回だけ丸めた場合と値がずれます。
パッケージの外で正しく実装しようとすると、次の3通りしかありません。

  1. 純 Go: 積を float64 で正確に計算し、Boldo & Melquiond の round-to-odd という手法で二重丸めを補正する。結果は正しいものの、1命令の数倍のコストがかかり、実装も難しい。
  2. ビルドタグ付きで素の x*y + z を書く: arm64 などでは gc コンパイラが自動で FMA に融合するので速い。ただし言語仕様はこの融合を「許可」しているだけで「保証」はしていません。そのため次の場合に気づかないまま壊れます。
    • GOCOMPILEDEBUG=fmahash で融合箇所を二分探索するとき
    • gc 以外のツールチェーンを使うとき
    • コンパイラの融合ルールが変わったとき
  3. アセンブリ: 結果は正しいが、インライン化できない。呼び出しのオーバーヘッドが演算そのものより何倍も大きくなる。

提案された解決策

// FMA32 returns x * y + z, computed with only one rounding.
// (That is, FMA32 returns the fused multiply-add of x, y, and z.)
func FMA32(x, y, z float32) float32
  • FMA と同じようにコンパイラが intrinsify します。アーキテクチャごとの扱いは次のとおりです。
    • arm64 / loong64 / ppc64x / riscv64 / s390x: FMADDS などの単精度命令を常に使う
    • amd64: VFMADD231SS を使う。GOAMD64>=v3 でなければ実行時に CPU の対応を確認する
    • arm: VFPv4 に対応しているかを実行時に確認する
    • 386 / mips / wasm: 純 Go のフォールバックを使う
  • フォールバックは float64 のハードウェア演算を数回使うだけです。整数演算で組んでいる float64 FMA のフォールバックよりかなり安くなります。
  • 名前は Nextafter32 の命名に合わせています。特殊値(NaN や Inf など)の扱いは IEEE 754 の fusedMultiplyAdd に従います。

これによって何ができるようになるか

  • SIMD パッケージ: simd/internal/spec の参照実装では、float32 の MulAdd を1回丸めの FMA と定義しています(CL 855743)。性能が重要な可搬版 simd でも、intrinsify された FMA32 をそのまま使えるようになります。
  • 数値計算・グラフィックス・機械学習: float32 を中心に扱うコードで、どのプラットフォームでもビット単位で同じ結果を得られます。
  • 誤差補償アルゴリズム: 補償付き総和や double-float 演算など、FMA が厳密であることを前提にした float32 のアルゴリズムを、速度を落とさずに書けます。

コード例

// Before: 二重丸めになり、まれに誤った結果になる
r := float32(math.FMA(float64(x), float64(y), float64(z)))
// Before: 速いが、コンパイラ最適化が効いたときしか正しくない
r := x*y + z // 融合されるかはアーキテクチャやコンパイラ次第
// After: 全プラットフォームで1回丸めが保証され、対応ハードウェアでは1命令になる
r := math.FMA32(x, y, z)

議論のハイライト

  • math に入れる理由: float32 版の math32 パッケージを提案した #45915 は却下されました。そのときの理由は「Sin や Cos はコンパイラの組み込み関数を使っていないので、外部ライブラリで十分」というものでした。FMA32 は反対に、コンパイラが命令を直接出せないと意味がないケースなので、標準ライブラリに入れる意味があります。
  • 「コンパイラが融合してもよい」と「必ず融合する」の違い: math.FMA を採用した #25819 と同じ論点です。コンパイラが融合することを、厳密な FMA の代わりにはしないという過去の判断を踏襲しています。
  • 却下された代替案:
    • gc で float32 の x*y + z の融合を保証する案。命令のないプラットフォームでは保証できず、fmahash による二分探索とも両立しません。
    • FMA をジェネリック化する案。互換性を壊すうえ、math パッケージの他の関数とも一貫しません。
  • フォールバックの最適化: @thatnealpatel が改良案を出しました。
    • 元の round-to-odd 実装は、仮数部の最下位ビットを見て約50%の確率で補正処理に入っていました。
    • 改良案は「float64 の下位28ビットがすべて0」、つまり結果が2つの float32 のちょうど中間にある可能性がある場合にだけ補正します。
    • これでベンチマークの幾何平均が約25%速くなりました。
    • @aclements はこの判定を定数名付きの読みやすい形に書き直しました。改良版も TestFloat-3e に合格しています。

関連リンク