Skip to content

fixed 1.3.2 — fixMul stops paying per inlined call at -O0

Choose a tag to compare

@gafferongames gafferongames released this 25 Aug 02:48
· 12 commits to main since this release
0612896

A pure speed change to unoptimized builds, and the frozen hashes prove it: every one in the repository is unchanged, on both 128-bit arms, so fixMul returns the same bits it always did.

What was slow. The 128-bit seam in v1.3.0 gave every 128-bit operation a named FIX_ALWAYS_INLINE function, and fixMul became three of them:

fixInt128 product = fixInt128MulI64( a, b );
fixInt128 r = fixInt128Shr( fixInt128Add( product, fixInt128FromI64( FIX_HALF ) ), FIX_FRACTION_BITS );

"Are they inlined?" was the wrong question — both spellings are. always_inline substitutes a body; it does not optimize it. At -O0 the substituted body still materializes its operand and its result to a stack slot, so one expression on the most frequently executed function in the library became three inlined calls with stores and reloads between them. -O2 folds all of it, which is why optimized builds never showed the problem and only unoptimized ones regressed — measured at roughly 2x for a consumer's whole debug test run, and far worse under a sanitizer that instruments exactly that stack traffic.

What it is now. fixMul spells the rounding expression a second time in native operators, under the existing #if FIX_INT128_EMULATED arm selection. The emulated arm — plain MSVC, and anything built with FIX_FORCE_EMULATED_INT128 — is untouched: it is doing real work and has nothing to fold. Visual C++ support is unchanged.

The native spelling is a transcription of the seam's own native bodies rather than a second algorithm: fixInt128MulI64( a, b ) is (fixInt128)a * b, fixInt128Add( x, y ) is (fixInt128)( (fixUInt128)x + (fixUInt128)y ), fixInt128Shr( x, n ) is x >> n. The unsigned round trip in the add comes across with everything else — it is what keeps the overflow behavior defined and the two arms in agreement at the boundary. The saturation tail stays on the seam: off by default, cold when on.

Measured, on fixed3d's Debug suite with BOX3D_VALIDATE=ON, three interleaved runs each: 39.4 / 40.4 / 40.8 s before, 24.2 / 23.6 / 24.7 s after. About 40% off an unoptimized consumer's test run, and roughly four fifths of the regression that arrived with the seam.

Optimized builds do not move, and that is checked rather than asserted: building fixed3d at -O2 against the old and the new header produces 83 of 83 project object files byte-identical.

Bit-identity is the whole suite, not a spot check: every test binary on both arms, negative controls included, run before and after with their full output diffed — identical apart from the self-reported wall-clock lines. Clean under UBSan on both arms.

The seam header and USAGE.md now name this exemption where the "no bare 128-bit operator" rule is stated, so the count of exceptions stays visible.

The remainder of the unoptimized regression — the fixInt128 reductions in fixed_vec.h — is tracked in #18, which stays open.