Skip to content

Add SIMD emulation of mul_add_precise for f64 on SSE4.2 - #324

Open
Shnatsel wants to merge 6 commits into
linebender:mainfrom
Shnatsel:mul_add_precise_f64
Open

Add SIMD emulation of mul_add_precise for f64 on SSE4.2#324
Shnatsel wants to merge 6 commits into
linebender:mainfrom
Shnatsel:mul_add_precise_f64

Conversation

@Shnatsel

@Shnatsel Shnatsel commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The implementation is based on a 2025 paper by Graillat and Muller, Emulation of the FMA and the correctly-rounded sum of three numbers in rounding-to-nearest floating-point arithmetic. Integrating cutting edge research here!

To the best of my knowledge this doesn't fix any libm or musl bugs, so this isn't necessary for correctness, it's just an optimization.

This is upwards of 3x faster than scalar on normal values; huge values and subnormals fall back to scalar and only get about 0.7x the usual scalar performance. It's still worth it because subnormals are rare and slow even in hardware.

@awxkee I'd appreciate it if you could take a look

@awxkee awxkee left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Dekker product with Veltkamp's split is no doubts impossible for edge cases which the paper covers only with a blanket "provided that underflow and overflow do not occur", and instead it use fast2mult using FMA in annex to compute FMA, what is a little bit ironic. Since values that are impossible to split are gated here, everything else seems correct to me.

@Shnatsel

Copy link
Copy Markdown
Contributor Author

Thanks a lot!

@Shnatsel Shnatsel changed the title Add SIMD emulation of mul_add_precise for SSE4.2 Add SIMD emulation of mul_add_precise for f64 on SSE4.2 Aug 11, 2026
The kernel now magnitude-sorts both error-free additions and uses Fast2Sum, shortening their dependency chains. The correction predicate also omits redundant exponent classification under the existing safe-range proof.

Also tried and rejected an exponent-bit correction kernel and a one-blend Fast2Sum formulation because they regressed the real benchmark.

Here's what llvm-mca thinks:

| CPU model | Original throughput | Optimized throughput | Modeled total cycles |
|---|---:|---:|---:|
| Zen 4 | 11.5 → 9.5 | 17% better | 41.1 → 36.1 |
| Nehalem | 24 → 20 | 17% better | 53.1 → 49.1 |
| Tremont | 47 → 37 | 21% better | 108.0 → 87.1 |
…ance by 25% in time and 33% in throughput on Zen4 benchmarks and showing llmv-mca improvements across the board
@Shnatsel
Shnatsel force-pushed the mul_add_precise_f64 branch from ab19a76 to 9e4a6cf Compare August 12, 2026 15:06
@Shnatsel
Shnatsel marked this pull request as ready for review August 12, 2026 15:06
@Shnatsel

Copy link
Copy Markdown
Contributor Author

Now that #323 is merged, I've rebased it on main and this is now ready for review

@LaurenzV LaurenzV left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I obviously haven't tried to verified this. 😄

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we again move the ignored tests to the bottom?

b: f64x2<Sse4_2>,
c: f64x2<Sse4_2>,
) -> f64x2<Sse4_2> {
let a_raw: __m128d = a.into();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For such a long function, is it really good to inline everything? Should we maybe do an inline never and use simd.vectorize here somehow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function calls are quite expensive with SIMD arguments so I don't think #[inline(never)] is a good idea. But I've been thinking about inlining and vectorize recently and I do believe there is a way to do it that'll be a good fit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further consideration, kernel! already desugars into a function declaration with #[inline] and #[target_feature] on it, but not #[inline(always)].

Outlining this sounds like a good idea because it could reduce register pressure and avoid spills. However, functions calling SIMD are quite slow because all the SIMD vectors need to be spilled to the stack, they cannot be passed in registers. A change to pass them in registers was made in Rust 1.87 but it broke inlining across functions with compatible but not identical #[target_feature] annotations and caused massive regressions.

I don't think it's a good idea to force register spills here out of fear it might cause register spills if we don't force them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants