On Mon, 19 Oct 2026, Richard Biener wrote:
> Hmm, so what you actually want is -ffp-contract=standard to also
> cover expressions using intrinsics? I think that's a reasonable
> extension and could possibly be achieved with a target hook
> helping to identify plus/mult intrinsic calls? So I'd solve this
> particular issue from within the frontend paths implementing
> -ffp-contract=standard instead.
Yes, and I've prototyped it in the c-family code that implements
-ffp-contract=on:
- vector float types are contracted like scalars (today only
SCALAR_FLOAT_TYPE_P is);
- a new hook, TARGET_FP_CONTRACT_CALL_CODE, returns PLUS_EXPR,
MINUS_EXPR or MULT_EXPR for an intrinsic that is exactly that
operator. The x86 version accepts _mm*_{add,sub,mul}_p{s,d}, so
_mm256_add_pd (_mm256_mul_pd (a, b), c) becomes .FMA.
Much C++ SIMD code goes through wrapper types instead (for example
vectorclass's Vec4d, or a struct around a double), where a * b + c
calls the class's operator* and operator+, which no target hook can
know about. For those the prototype adds an fp_contract attribute.
On operator*, operator+, operator-, operator+= or operator-= of a
class holding a single floating-point value, it tells the front end
the call is that operator on the value, so it is contracted like the
built-in one. Like const, it is a promise: the body is not looked at.
Marking vectorclass's same-type operators makes both a * b + c and
result += a * b contract.
One more issue with -ffp-contract=on: s += x[i] * y[i] is
contracted, so the loop becomes a chain of FMAs, and an FMA has a
longer latency than an add. With fast the vectorizer splits it into
vector multiplies and in-order adds, so it loses about 2x on a plain
dot product. With several independent accumulators fusing wins
instead. The prototype leaves an accumulation x = x +- a * b
uncontracted when it is the only one in its loop body. That is
decided in the front end, so it stays deterministic, and it matches
fast on both shapes.
Does the attribute look acceptable, or would you cover wrapper types
differently? And would a rule like the accumulation one be acceptable
in -ffp-contract=on, or should it be a separate option? If both are
fine, I'd send four patches: vector types, the hook, the attribute,
the accumulation rule.
Thanks,
Paavan
On Mon, Oct 19, 2026 at 12:21 PM Richard Biener <[email protected]> wrote:
> On Fri, 16 Oct 2026, Paavan Indela wrote:
>
> > On Fri, Oct 16, 2026, Richard Biener wrote:
> >
> > It must be after IPA since it otherwise may use the wrong target when
> > optimizing functions marked for offloading.
> >
> > Agreed. The revised version keeps contraction in widening_mul, after IPA
> > and vectorization.
> >
> > 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
> >
> > Agreed, so the boundary is now an annotation. Revised proposal:
> >
> > -ffp-contract=local fuses a * b + c only when the multiply and the add
> come
> > from the same function, counting always_inline callees as part of their
> > caller. No other call boundary is crossed, whether or not the call gets
> > inlined. x86 intrinsics are already always_inline; wrapper types mark
> their
> > operators. __builtin_assoc_barrier still opts out locally.
> >
> > The prototype does this in convert_mult_to_fma:
> >
> > - the scope of a statement is its innermost inlined-function block that
> is
> > not always_inline (BLOCK_ORIGIN; these blocks are kept at -g0, so -g does
> > not change the result);
> > - a use in a dominated block qualifies, not only the same block, so a
> call
> > inlined between the multiply and the add changes nothing;
> > - loop-carried values stay unfused, so vector and scalar code agree.
> >
> > One case still leaks: after IPA inlining, FRE can merge an identical a *
> b
> > from the caller and the callee into one multiply that spans both scopes,
> so
> > neither side fuses. Is that acceptable as a documented limitation, or
> would
> > you handle it differently?
>
> Hmm, so what you actually want is -ffp-contract=standard to also
> cover expressions using intrinsics? I think that's a reasonable
> extension and could possibly be achieved with a target hook
> helping to identify plus/mult intrinsic calls? So I'd solve this
> particular issue from within the frontend paths implementing
> -ffp-contract=standard instead.
>
> Richard.
>
>
> > Thanks,
> > Paavan Kumar
> >
> > On Fri, Oct 16, 2026 at 4:14 PM Richard Biener <[email protected]>
> wrote:
> >
> > >
> > >
> > > 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
> > >
> > >
> >
>
> --
> Richard Biener <[email protected]>
> SUSE Software Solutions Germany GmbH,
> Frankenstrasse 146, 90461 Nuernberg, Germany;
> GF: Stefan Gaiser, Jochen Jaser, Abhinav Puri; (HRB 36809, AG Nuernberg)