#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通りしかありません。
- 純 Go: 積を float64 で正確に計算し、Boldo & Melquiond の round-to-odd という手法で二重丸めを補正する。結果は正しいものの、1命令の数倍のコストがかかり、実装も難しい。
- ビルドタグ付きで素の
x*y + zを書く: arm64 などでは gc コンパイラが自動で FMA に融合するので速い。ただし言語仕様はこの融合を「許可」しているだけで「保証」はしていません。そのため次の場合に気づかないまま壊れます。GOCOMPILEDEBUG=fmahashで融合箇所を二分探索するとき- gc 以外のツールチェーンを使うとき
- コンパイラの融合ルールが変わったとき
- アセンブリ: 結果は正しいが、インライン化できない。呼び出しのオーバーヘッドが演算そのものより何倍も大きくなる。
提案された解決策
// 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 のフォールバックを使う
- arm64 / loong64 / ppc64x / riscv64 / s390x:
- フォールバックは 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パッケージの他の関数とも一貫しません。
- gc で float32 の
- フォールバックの最適化: @thatnealpatel が改良案を出しました。
- 元の round-to-odd 実装は、仮数部の最下位ビットを見て約50%の確率で補正処理に入っていました。
- 改良案は「float64 の下位28ビットがすべて0」、つまり結果が2つの float32 のちょうど中間にある可能性がある場合にだけ補正します。
- これでベンチマークの幾何平均が約25%速くなりました。
- @aclements はこの判定を定数名付きの読みやすい形に書き直しました。改良版も TestFloat-3e に合格しています。