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)?
4. Name: local, early, stable?
Thanks,
Paavan Kumar