> Am 16.10.2026 um 12:39 schrieb paavan kumar via Gcc <[email protected]>:
>
>
> Hi,
>
> Before writing a patch, I'd like to know if an opt-in mode like this would be
> welcome.
>
> With -ffp-contract=fast, whether a * b + c gets fused depends on IPA
> inlining, so unrelated code can change the result of a function that didn't
> change. Small example (trunk, -O2 -mfma):
>
> static double
> scale (const double *w, double a, double b)
> {
> double k = 0;
> for (int i = 0; i < 16; i++)
> k += w[i] * w[15 - i] + w[(i * 5) & 15] / (1.0 + w[i]);
> if (k == 12345.0)
> b = k;
> return a * b;
> }
>
> __attribute__ ((noipa)) double
> f (const double *w, double a, double b, double c)
> {
> return scale (w, a, b) + c;
> }
> f (w, 1 + 0x1p-27, 1 - 0x1p-27, -1) returns -0x1p-54. Add an unrelated second
> caller of scale and it returns 0 (see also PR106902, PR126195). We keep
> hitting this in a large C++ code base: a header change flips a vfmadd in code
> it doesn't touch, and results differ between builds that should match.
>
> -ffp-contract=on fixes the determinism but only fuses within one scalar
> expression. It can't see through operators of wrapper types or intrinsics
> like _mm256_add_pd (_mm256_mul_pd (a, b), c), so that kind of code loses most
> of its FMAs.
>
> Proposal: -ffp-contract=local. Form FMAs in a small pass at the end of the
> early optimizations (after early inlining, before IPA), reusing
> convert_mult_to_fma. No other pass contracts in this mode. Fusion then
> depends only on the function and what early inlining put into it, not on
> later inlining, cloning, LTO or unrelated code. In the example f returns 0
> either way.
>
> Not fused: values carried around a loop (see below) and operations that only
> appear after IPA (complex lowering, vectorizer). Opt out per function with
> optimize ("fp-contract=off"). Not a guarantee across GCC versions.
>
> Numbers from a prototype (i5-13420H, -O3 -march=haswell, ns/element):
>
> off on fast local
> x = x * a + b 1.850 1.060 1.060 1.060
> dot, 1 accumulator 0.520 1.040 0.520 0.520
> dot, 8 accumulators 0.165 0.136 0.135 0.165
> Horner, double 0.430 0.250 0.250 0.250
> Horner, wrapper struct 0.420 0.420 0.240 0.240
> Richard, you've said FMAs belong after vectorization, and the dot rows show
> why. fast vectorizes the 1-accumulator dot as vector multiplies plus in-order
> scalar adds. An .FMA in that chain can't be split (PR120823), so it stays a
> scalar FMA chain at 2x; on pays that today. So the prototype doesn't fuse
> into loop-carried values, which in turn costs the 8-accumulator case that
> fast fuses after vectorization. Deciding before vectorization means picking
> one.
>
> Also open: targets with scalar but no vector FMA (maybe contract early only
> when the vector FMA exists too), and size heuristics moving with early .FMA
> (at -O2 an inner Horner loop was fully unrolled under local but not under
> fast).
>
> Questions:
>
> 1. Is an opt-in mode like this acceptable, or would you solve it differently?
> 2. Is not fusing loop-carried values the right default?
> 3. Is the end of the early optimizations the right place (it must be before
> pass_split_functions)?
>
It must be after IPA since it otherwise may use the wrong target when
optimizing functions marked for offloading.
Also allowing early inlining but not IPA is odd and feels like a heuristic
specifically tuned to the problem. As you do want to form FMA across calls I
don’t think the issue can be solved without annotation with for example
__builtin_assoc_barrier
Richard
> 4. Name: local, early, stable?
>
> Thanks,
> Paavan Kumar