Skip to content

Add #[simd] macro - #347

Open
Shnatsel wants to merge 22 commits into
linebender:mainfrom
Shnatsel:simd-macro
Open

Add #[simd] macro#347
Shnatsel wants to merge 22 commits into
linebender:mainfrom
Shnatsel:simd-macro

Conversation

@Shnatsel

@Shnatsel Shnatsel commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

My attempt at #338

I am not well-versed in proc macros. While the design of what the macro should expand into is mine, the implementation of the macro and thinking through various edge cases and handling them was largely delegated to Codex (GPT-5.6 Sol Ultra).

Codex summary of the changes

Implemented the standalone #[simd] proc macro.

Key pieces:

  • Added publishable fearless_simd_macros 0.1.0 with Syn 3.0.4 and no core dependency: implementation, documentation.
  • Exact tail expansion to token.vectorize(#[inline(always)] || { ... }), with no generated outer inline attribute.
  • Implemented token selection, wildcard hygiene, attribute preservation, supported function forms, and all specified diagnostics.
  • Updated generated vectorize helpers and removed the stale outlined-boundary documentation.
  • Added behavioral, no-std, and Trybuild coverage: behavioral tests, UI tests.
  • Updated CI, READMEs, changelog, MSRV checks, and release-order documentation.
  • Core’s normal dependency tree remains unchanged.

Verification passed:

  • Full workspace suite on Rust 1.89: 10,304 main harness tests passed, 115 ignored.
  • Clippy and rustdoc with -D warnings.
  • 2 Trybuild pass cases and 15 expected failures.
  • x86_64-unknown-none no-std build.
  • Wasm SIMD/fallback and AArch64 NEON/fallback runtime tests.
  • Generator and cargo-rdme consistency.
  • Offline macro package assembly and verification.
  • Formatting and git diff --check.

Existing unrelated untracked workspace files were left untouched.

Before implementing the macro I investigated vectorize() and found that it already gets inlined most of the time anyway. It doesn't work well as an inlining barrier, since the function that isn't annotated #[inline] is trival enough for the optimizer to always inline it anyway, even with a large closure body. So I reverted a separate vectorize_inline() which is recorded in commit history, and inlining can instead be controlled with annotation on the function calling vectorize(); the docs on it already show that usage in the example, the doc comment on vectorize() can probably be improved.

The proc macro crate is separate from fearless_simd so it doesn't fall under the v1.0 guarantees. I expect to publish it in lockstep with v1.0, although we might ship pre-releases just to make sure it all works. The README points to git but should work with most versions no problem, just wouldn't have as much inlining annotations in vectorize().

@LaurenzV

Copy link
Copy Markdown
Collaborator

I think let's wait until linebender/vello#1853 has been merged and then we can try the ergonomics of using it there?

…h the documented use case for vectorize(). Does not affect usage in dispatch!() which dispatches from non-target-feature context. The function itself was already trivial and under -O3 the optimizer would recognize it as trivial and inline it anyway, so adding a function boundary there didn't really work as intended, except under -Os.
@Dr-Emann

Copy link
Copy Markdown
Contributor

Woudln't mind the #[inline] on the inside of vectorize (and/or eliminating the inner call when at baseline), I'm seeing some useful performance impact on aarch64 by doing it kinda manually with:

#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
if simd.level().as_neon().is_some() {
    return inner_code(a, b, c);
}
simd.vectorize(
    #[inline(always)]
    move || inner_code(a, b, c);
}

I believe it's somewhat exaggerated by taking enough arguments, in:

        #[target_feature(enable = "neon")]
        fn vectorize_neon<F: FnOnce() -> R, R>(f: F) -> R {
            f()
        }
        unsafe { vectorize_neon(f) }

vectorize_neon gets a single closure struct containing a, b, and c, which spills to the stack, rather than being passed as registers which passing as arguments can do.

Wonder if there's some macro magic we could do in the #[simd] impl that could use real arguments, rather than delegating to .vectorize(closure)

@Dr-Emann

Copy link
Copy Markdown
Contributor

And/or, maybe just special casing a few arguments, and doing an annoying simd.vectorize_1(a, #[inline(always)] |a| { ... }), simd.vectorize_2(a, b, #[inline(always)] |a, b| { ... }), ...

…at it opens up an opportunity to spoof fearless_simd types, which would be unsound, move the unsafe blocks into a declarative macro inside fearless_simd so that the proc macro emits no unsafe code itself.
@Shnatsel

Copy link
Copy Markdown
Contributor Author

Ugh, turns out closure-captured arguments get put into a struct that then gets spilled to the stack.

I've rewritten the macro to avoid that with Astra's assistance.

The proc macro now also needs to refer to fearless_simd crate explicitly, and to avoid making it unsound by spoofing fearless_simd crate I had to move all the unsafe into a declarative macro inside fearless_simd itself.

I think it's a reasonable split going forward, especially since all the level matching is now happening inside fearless_simd where it can all be updated in lockstep if/when we add new levels. Alternatively we can make renaming fearless_simd harder by using ::fearless_simd in the proc macro output and this will require renaming it in Cargo.toml; technically a soundness hole if you do that, but not a practical one.

@Shnatsel

Copy link
Copy Markdown
Contributor Author

tests fail because trybuild is too strict and the changes to the compiler's diagnostic output between versions trip it up. The current snapshots expect 1.97 output while CI runs on 1.89.

There is no fuzzy matching functionality in trybuild so we might have to use something else for testing the macro.

@Shnatsel

Copy link
Copy Markdown
Contributor Author

I've fixed the issues around trybuild being overly strict for UI tests by migrating to ui_test

The README.md is quite poor and needs to be rewritten, but I'll deal with that once the code settles down

@Shnatsel

Copy link
Copy Markdown
Contributor Author

I feel a lot more confident in this code now that I've moved all the safety-critical bits into a straightforward and comprehensible declarative macro. Well, comprehensible by macro standards, anyway.

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