rust-lang / rust-lang/rust

Very bad SIMD code generation for simple Euler integration

Open
#145,248 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-autovectorization A-SIMD C-bug I-slow O-x86_64 T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

I tried this code:

use std::ops::AddAssign;
use std::iter::zip;
use std::ops::Mul;

#[derive(Clone, Copy)]
pub struct Vector<T, const N: usize>([T; N]);

impl<T: Mul<Output = T> + Copy, const N: usize> Mul<T> for Vector<T, N> {
    type Output = Self;
    fn mul(self, other: T) -> Self {
        Self(self.0.map(|s| s * other))
    }
}
impl<T: AddAssign, const N: usize> AddAssign for Vector<T, N> {
    fn add_assign(&mut self, other: Self) {
        for (d, s) in zip(&mut self.0, other.0) {
            *d += s;
        }
    }
}

type Vec4f = Vector<f32, 4>;

#[unsafe(no_mangle)]
pub fn euler(x: &mut [Vec4f], v: &mut [Vec4f], a: &mut [Vec4f], dt: f32) {
    if x.len() != v.len() || v.len() != a.len() {
        return;
    }

    for ((x, v), a) in zip(zip(x, v), a) {
        *v += *a * dt;
        *x += *v * dt;
    }
}

I expected to see this happen: When targeting x86-64, the generated code should load, process, and store one Vec4f at a time, using SSE (potentially unrolled).

The compiler does in fact generate such code, but it is only used to process the remainder of the slices, when there are fewer than 4 Vec4fs left:

.LBB0_7:                                # =>This Inner Loop Header: Depth=1
	movups	xmm1, xmmword ptr [r8 + rax]
	mulps	xmm1, xmm0
	movups	xmm2, xmmword ptr [rdx + rax]
	addps	xmm2, xmm1
	movups	xmmword ptr [rdx + rax], xmm2
	mulps	xmm2, xmm0
	movups	xmm1, xmmword ptr [rdi + rax]
	addps	xmm1, xmm2
	movups	xmmword ptr [rdi + rax], xmm1
	add	rax, 16
	dec	rsi
	jne	.LBB0_7

Instead, this happened: In a desperate attempt at unrolling the loop to process 4 Vec4fs at a time, the compiler generates code that loads each f32 individually and spends more time loading and shuffling lanes than actually doing useful math.

.LBB0_4:                                # =>This Inner Loop Header: Depth=1
	movss	xmm2, dword ptr [r8 + rcx + 48] # xmm2 = mem[0],zero,zero,zero
	movss	xmm3, dword ptr [r8 + rcx + 32] # xmm3 = mem[0],zero,zero,zero
	unpcklps	xmm3, xmm2                      # xmm3 = xmm3[0],xmm2[0],xmm3[1],xmm2[1]
	movss	xmm4, dword ptr [r8 + rcx + 16] # xmm4 = mem[0],zero,zero,zero
	movss	xmm2, dword ptr [r8 + rcx]      # xmm2 = mem[0],zero,zero,zero
	unpcklps	xmm2, xmm4                      # xmm2 = xmm2[0],xmm4[0],xmm2[1],xmm4[1]
	movlhps	xmm2, xmm3                      # xmm2 = xmm2[0],xmm3[0]
	movss	xmm7, dword ptr [r8 + rcx + 4]  # xmm7 = mem[0],zero,zero,zero
	movss	xmm6, dword ptr [r8 + rcx + 8]  # xmm6 = mem[0],zero,zero,zero
	movss	xmm5, dword ptr [r8 + rcx + 12] # xmm5 = mem[0],zero,zero,zero
	movss	xmm3, dword ptr [r8 + rcx + 52] # xmm3 = mem[0],zero,zero,zero
	movss	xmm4, dword ptr [r8 + rcx + 36] # xmm4 = mem[0],zero,zero,zero
	unpcklps	xmm4, xmm3                      # xmm4 = xmm4[0],xmm3[0],xmm4[1],xmm3[1]
	movss	xmm3, dword ptr [r8 + rcx + 20] # xmm3 = mem[0],zero,zero,zero
	unpcklps	xmm7, xmm3                      # xmm7 = xmm7[0],xmm3[0],xmm7[1],xmm3[1]
	movlhps	xmm7, xmm4                      # xmm7 = xmm7[0],xmm4[0]
	movss	xmm3, dword ptr [r8 + rcx + 56] # xmm3 = mem[0],zero,zero,zero
	movss	xmm4, dword ptr [r8 + rcx + 40] # xmm4 = mem[0],zero,zero,zero
	unpcklps	xmm4, xmm3                      # xmm4 = xmm4[0],xmm3[0],xmm4[1],xmm3[1]
	movss	xmm3, dword ptr [r8 + rcx + 24] # xmm3 = mem[0],zero,zero,zero
	unpcklps	xmm6, xmm3                      # xmm6 = xmm6[0],xmm3[0],xmm6[1],xmm3[1]
	movlhps	xmm6, xmm4                      # xmm6 = xmm6[0],xmm4[0]
	movss	xmm3, dword ptr [r8 + rcx + 60] # xmm3 = mem[0],zero,zero,zero
	movss	xmm4, dword ptr [r8 + rcx + 44] # xmm4 = mem[0],zero,zero,zero
	unpcklps	xmm4, xmm3                      # xmm4 = xmm4[0],xmm3[0],xmm4[1],xmm3[1]
	movss	xmm3, dword ptr [r8 + rcx + 28] # xmm3 = mem[0],zero,zero,zero
	unpcklps	xmm5, xmm3                      # xmm5 = xmm5[0],xmm3[0],xmm5[1],xmm3[1]
	movlhps	xmm5, xmm4                      # xmm5 = xmm5[0],xmm4[0]
    ; <MANY lines omitted...>

This also happens when adding #[repr(align(16))] to Vector, so it is not related to unaligned operations.

Meta

Reproduces on the playground, on both stable 1.89.0, and nightly 2025-08-10.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the euler reproducer from the issue and compare its x86-64 assembly output on the playground or the reported stable and nightly versions. Investigate why the unrolled loop uses scalar loads and lane shuffles instead of vector loads; done means the generated loop processes Vec4f values with efficient SIMD instructions.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.