{"article":{"slug":"safe-simd-in-rust-even-on-the-inside","title":"Safe SIMD in Rust, even on the inside","subtitle":null,"summary":"Sergey Davidoff explains why raw SIMD intrinsics force unsafe code in Rust and how the fearless_simd crate removes unsafe even from its own internals, using CPU feature tokens, generics, and aggressive inlining while working around ABI pitfalls.","content_type":"blog_post","language":"en","canonical_url":"https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/","author":{"name":"Sergey Davidoff","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Sergey \"Shnatsel\" Davidoff","url":"https://shnatsel.github.io/","listing_slug":null,"listing":null},"topics":[{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2175,"reading_minutes":9,"published_at":"2026-06-19T00:00:00.000Z","added_at":"2026-10-11T23:10:04.572Z","updated_at":"2026-10-11T23:10:04.572Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/safe-simd-in-rust-even-on-the-inside","markdown_url":"https://listedarticles.com/articles/safe-simd-in-rust-even-on-the-inside.md","example":false,"citation":"Sergey Davidoff, Sergey \"Shnatsel\" Davidoff. \"Safe SIMD in Rust, even on the inside.\" 19 Jun 2026. https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://shnatsel.github.io/safe-simd-in-rust-even-on-the-inside/"},"body_markdown":"*Rust’s* [*SIMD abstractions*](https://shnatsel.medium.com/the-state-of-simd-in-rust-in-2025-32c263e5f53d) *were not as safe as I’d like. Until now.*\n\nIt’s no secret that raw SIMD intrinsics are unpleasant to use.\n\nYou want to write `a + b`, not this monstrosity:\n\n```\nunsafe {\n    #[cfg(all(any(target_arch = \"x86\", target_arch = \"x86_64\"), target_feature = \"avx2\"))]\n    _mm256_add_ps(a, b)\n    #[cfg(all(any(target_arch = \"x86\", target_arch = \"x86_64\"), target_feature = \"sse\", not(target_feature = \"avx2\")))]\n    _mm_add_ps(a, b)\n    #[cfg(all(target_arch = \"aarch64\", target_feature = \"neon\"))]\n    vaddq_f32(a, b)\n}\n```\n\nLook at it. It’s hideous. And the whole thing is wrapped in `unsafe`!\n\nAnd that’s a simplified example. It still doesn’t handle:\n\n* Other common platforms: AVX-512, 32-bit ARM, WebAssembly* Platforms without SIMD or obscure platforms like RISC-V* Actually loading data like `&[f32]` into a form that each intrinsic accepts* Selecting the best implementation for the CPU it’s running on\n\nLuckily, Rust provides [many SIMD abstractions](https://shnatsel.medium.com/the-state-of-simd-in-rust-in-2025-32c263e5f53d) that handle all of that for you and let you simply write `a + b`.\n\nThere is just one wrinkle. Inside, they’re still full of `unsafe`. It wasn’t gone, just hidden. Vast quantities of it lurking just beneath the surface, getting [screwed](https://github.com/servo/pathfinder/issues/588) [up](https://github.com/wingertge/macerator/issues/31) [occasionally](https://github.com/sarah-quinones/pulp/issues/28).\n\nOr rather, they were. Until now.\n\n## Why do we even need ‘unsafe’?\n\nFor the longest time you couldn’t get around wrapping the call to each intrinsic function such as `_mm256_add_ps` into `unsafe` because it is illegal to call one when it’s not available on the CPU you’re running on.\n\nSo you *had* to have some mechanism for tracking which instructions are needed for each intrinsic, and which instructions you have access to, and cross-referencing them to decide if it’s safe to call a given function.\n\nIt was either tedious if done by hand or complex if done by a code generator, always error-prone, and required `unsafe` around every intrinsic.\n\nThis changed in Rust 1.87 when the compiler started tracking the required instruction sets itself, so you could write this:\n\n```\n#[target_feature(enable = \"avx2\")]\nfn add_avx2(a: __m256, b: __m256) -> __m256 {\n    _mm256_add_ps(a, b) // this is an avx2 intrinsic\n}\n```\n\nLook ma, no `unsafe`!\n\n…yet.\n\nYou still cannot write `a + b` with this. The best you can do is this:\n\n```\nunsafe { add_avx2(a, b) }\n```\n\nThis only shifts the `unsafe` up a layer. You can call intrinsics inside functions annotated with the correct `#[target_feature]` now, but there still has to be `unsafe` somewhere in the chain.\n\nThe other problem is more fundamental. You cannot put `#[target_feature]` on the implementation of `+` for your type, because `+` must be available always. So no `a + b` for us using this mechanism.\n\n## Lemma: CPU feature tokens\n\nTo understand how the final solution works, you first need to understand how CPU feature detection works.\n\nNormally, checking for a CPU feature like AVX2 is done at runtime using `is_x86_feature_detected!(\"avx2\")`. But we definitely don’t want to run this check every single time we add two numbers together — that would completely tank performance. We want to check it *once*, and then prove to the compiler that it’s safe to use AVX2 instructions from that point on.\n\nInstead we can encode this proof into the type system using an *unforgeable token*: a zero-sized type with a private inner field. The *only* way to obtain this token is to call a function that performs the CPU feature check. If the check passes, the function hands you the token:\n\n```\npub struct Avx2(());\n\nfn detect_avx2() -> Option<Avx2> {\n    if is_x86_feature_detected!(\"avx2\") {\n        Some(Avx2(()))\n    } else {\n        None\n    }\n}\n```\n\nAnd because it’s a zero-sized type, passing this token around has no runtime overhead. It exists purely as a compile-time proof.\n\nThe upshot is that as long as you have an instance of the `Avx2` struct, you can be sure that AVX2 instructions are available on the system.\n\n## The key insight\n\nThe compiler doesn’t know it, but this function is safe to call:\n\n```\n#[target_feature(enable = \"avx2\")]\nfn add_avx2(token: Avx2, a: __m256, b: __m256) -> __m256 {\n    _mm256_add_ps(a, b)\n}\n```\n\nYou can *only* call this function if you have an `Avx2` token, which you can only get if AVX2 instructions are available on the system.\n\nIf we can explain to the compiler that this is valid (using `unsafe`), we can write that `unsafe` only once and reuse it everywhere.\n\nWhat we need is a macro which is safe to invoke:\n\n```\nwith_avx2!(\n    fn add_avx2(token: Avx2, a: __m256, b: __m256) -> __m256 {\n        _mm256_add_ps(a, b)\n    }\n)\n```\n\nbut expands into this behind the scenes:\n\n```\nfn add_avx2(token: Avx2, a: __m256, b: __m256) -> __m256 {\n    // SAFETY: Avx2 is available according to the token,\n    // and we verified that the inner function is not an `unsafe fn`\n    unsafe { inner(token, a, b) }\n\n    #[target_feature(enable = \"avx2\")]\n    fn inner(token: Avx2, a: __m256, b: __m256) -> __m256 {\n        _mm256_add_ps(a, b)\n    }\n}\n```\n\nNow if you use an intrinsic that isn’t in AVX2, **the compiler will reject it!**\n\nWe’ve just managed to provide a safe programming interface to SIMD intrinsics without any bespoke tracking of target features!\n\nEven though there is one `unsafe` block still inside it, it’s encapsulated in a sound API, so you cannot misuse it to cause memory safety bugs. In that sense it is just like `println!`, safely abstracting unsafe code.\n\nThis way you only ever need to review and audit *this one macro,* not hundreds upon hundreds of bespoke `unsafe` blocks. And the only things we could *possibly* screw up in the implementation are:\n\n1. Mapping the token to the wrong `#[target_feature]`- Allowing an `unsafe fn` to be called from a safe context\n\nAnd both failure modes are quite easy to check for.\n\nSo now we can call `add_avx2(token, a, b)` without `unsafe`, but that still doesn’t get us to `a + b`. How do we solve *that?*\n\n## Generics to the rescue\n\nWe cannot annotate the implementation of `a + b` with `#[target_feature]` because it must be safe to call from anywhere. And we cannot pass a token into the function because it accepts `a` and `b` but not `token`.\n\nBut even if we could do that, it would make for a pretty ugly API. We want `a + b` to always work and automatically use the best SIMD instructions without the user ever fussing with tokens.\n\nWe can use generics to solve both problems at once: by defining an `f32x8` type that’s generic over the available instruction sets, we can implement addition on it that both smuggles a token inside it *and* creates a separate implementation for each SIMD instruction set!\n\nThis is what it looks like:\n\n```\npub trait Level {}\n\n#[derive(Clone, Copy)]\npub struct Avx2(());\nimpl Level for Avx2 {}\n\npub struct f32x8<L: Level> {\n    // For simplicity we'll back this with an array in this example.\n    // In production code we use native SIMD types for the level.\n    data: [f32; 8],\n    // The smuggled token!\n    token: L,\n}\n\n/// implementation of `a + b` for Avx2\nimpl std::ops::Add for f32x8<Avx2> {\n    type Output = Self;\n\n    fn add(self, rhs: Self) -> Self::Output {\n        // (type conversions abbreviated)\n        // Use the Avx2 token to call our safe wrapper\n        let result = add_avx2(self.token, self, rhs);\n        Self {\n            data: store_m256(result),\n            token: self.token,\n        }\n    }\n}\n```\n\nAnd then we can just as easily make it work for any other instruction set, or when SIMD is not available at all:\n\n```\n#[derive(Clone, Copy)]\npub struct NoSimd(());\nimpl Level for NoSimd {}\n\n/// implementation of `a + b` when no SIMD is available\nimpl std::ops::Add for f32x8<NoSimd> {\n    type Output = Self;\n\n    fn add(self, rhs: Self) -> Self::Output {\n        let result = std::array::from_fn(|i| self.data[i] + rhs.data[i]);\n        Self {\n            data: result,\n            token: self.token,\n        }\n    }\n}\n```\n\nWe’ve just solved safety *and* runtime instruction selection at once!\n\nAdd a convenience function that gives you the best `Level` available on the system, and you get pretty much the perfect API for SIMD!\n\n## The ABI would like a word\n\nThere is, unfortunately, a fundamental problem to writing `a + b` and having it lower to SIMD instructions: **function call overhead.**\n\nCalling a function is not free but pretty cheap — just a handful of CPU instructions. But a *handful* of instructions is a lot more than *one* instruction we’ve just used for implementing addition!\n\nSo if there’s a function call in the way, addition performance will plummet. And performance is the entire point of using SIMD in the first place!\n\nThe compiler is usually pretty good at erasing this overhead via [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html). It basically copy-pastes the implementation of a function you called into the function calling it, so there’s no more function and no more overhead.\n\nBut `#[target_feature]` annotations throw a wrench in the works. The compiler cannot inline a function that has a `#[target_feature]` annotation into one that doesn’t, because the required features are not available in it!\n\nAnd guess what cannot have a `#[target_feature]` annotation? Yeah.\n\nSo how *do* we make `a + b` work with SIMD?\n\n## Inline it, inline it with fire!\n\nWe cannot put `#[target_feature]` on the function that implements `a + b`, but we *can* put it on the function that calls `a + b`!\n\nThen we can use [inlining](https://matklad.github.io/2021/07/09/inline-in-rust.html) to get the implementation of `a + b` copied into the function calling it, and it ends up in a `#[target_feature]` context.\n\nSo the call chain looks like this:\n\n```\n#[target_feature(enable = \"avx2\")]\nfn do_stuff() {\n    // TODO: some computation\n    c = a + b;\n    // TODO: some more computation\n}\n\n// which calls into...\n\n#[inline(always)] // Function body will be copied into the caller\nfn add(self, rhs: Self) -> Self::Output {\n    // Use the Avx2 token to call our safe wrapper\n    add_avx2(self.token, self, rhs);\n    // return statement abbreviated\n}\n\n// which calls into...\n\n#[inline] // Function body will be copied into the caller if feasible\n#[target_feature(enable = \"avx2\")]\nfn add_avx2(token: Avx2, a: __m256, b: __m256) -> __m256 {\n    _mm256_add_ps(a, b)\n}\n```\n\nThis works.\n\nYou can add these annotations in the right places and abstract over the SIMD levels and you [don’t even need macros](https://docs.rs/fearless_simd/latest/fearless_simd/trait.Simd.html#tymethod.vectorize).\n\nThe problem is that whenever you call `a + b` on SIMD types, you have to do it from a function with either `#[inline(always)]` or `#[target_feature]` on it, otherwise the code still compiles but performance plummets.\n\nWanna see for yourself? Open [this example](https://rust.godbolt.org/z/xo1fEfqnT), remove `#[target_feature]` from it and watch the generated assembly turn into absolute horror show.\n\nI’m not sure what can be done about this. The limitation seems quite fundamental for any approach that implements `a + b` with SIMD.\n\nThe [Struct Target Features RFC](https://shnatsel.medium.com/github.com/rust-lang/rfcs/pull/3525) solves this for `add_avx2(token, a, b)` and `add::<S: Simd>(token, a, b)` but I don’t see a path to `a + b` just yet.\n\n## Where does that leave us?\n\nDespite the inlining wrinkle inherent to all SIMD code, we’ve managed to provide a remarkably pleasant SIMD abstraction at a staggeringly low, never-before-seen level of unsafe code.\n\nYou can find the production version of these ideas in [`fearless_simd`](https://crates.io/crates/fearless_simd) v0.5, **available now** in a package registry near you! And [here’s a small example](https://github.com/linebender/fearless_simd/blob/d2411665f0726cd6c09bd6fd98af4af18e3c1778/fearless_simd/examples/sigmoid.rs) to see how it all fits together in production.\n\nThe macro used to implement it is also exposed publicly, so you can easily [mix and match](https://github.com/linebender/fearless_simd/blob/e76b820154dc09f41bca8302670424d8ca079219/fearless_simd/examples/srgb.rs) high-level operations like `a + b` and platform-specific intrinsics to take full advantage of the hardware.\n\nThere is more than one `unsafe` block in `fearless_simd` because it also provides the functionality of [`safe_unaligned_simd`](https://crates.io/crates/safe_unaligned_simd) crate, but that too is done at a [significantly lower](https://github.com/okaneco/safe_unaligned_simd/issues/51) amount of unsafe code than the original.\n\nFor me the barrier to using high-level SIMD abstractions was always the sheer amount of `unsafe` they brought. It was scary and hard to justify.\n\nBut now SIMD in Rust can be truly fearless.\n\n## Acknowledgements\n\nI am shocked that I am the first to put this into production (as far as I can tell), because I’m certainly not the first person to think of this.\n\nCPU feature tokens are an old and common idea. The [`pulp`](https://crates.io/crates/pulp) crate has been using them for years, but they relied on handwritten unsafe wrappers around intrinsics, and [occasionally got them wrong](https://github.com/sarah-quinones/pulp/issues/28).\n\nGenerating multiple implementations using generics is also an old idea. It is part of the [original `fearless_simd` concept](https://linebender.org/blog/towards-fearless-simd/) from 8 years ago. The [`simdeez`](https://crates.io/crates/simdeez) crate, which predates it, also [seems to use something similar](https://docs.rs/simdeez/1.0.8/simdeez/).\n\nThe key insight of combining tokens with a single safe wrapper that delegates to `rustc` is not unique to me either. Just in the context of `fearless_simd` crate, Raph Levien has [experimented with it](https://github.com/linebender/fearless_simd/commit/81b6ab4c8dd6f25064f539c49818c45c2f686815), and [Daniel McNab](https://github.com/DJMcNab) created a [more elaborate implementation](https://github.com/linebender/fearless_simd/pull/108) than mine months ago.\n\nDaniel’s approach allows fine-grained tracking of every individual CPU feature, as opposed to a handful of fixed CPU feature levels that `fearless_simd` uses. It is more expressive, but came at the cost of complexity, and his approach never got merged because no other maintainer stepped up to review it. I still hope it will be published as a standalone crate someday.\n\nThanks to Daniel and to [Laurenz Stampfl](https://github.com/LaurenzV) for reviewing all my PRs to `fearless_simd`, they were big and the quick reviews are really appreciated!\n\n*This article was originally published [on my Medium](https://shnatsel.medium.com/safe-simd-in-rust-even-on-the-inside-c6f1ff381828).*","body_html":"<p><em>Rust’s</em> <a href=\"https://shnatsel.medium.com/the-state-of-simd-in-rust-in-2025-32c263e5f53d\" rel=\"nofollow ugc noopener\"><em>SIMD abstractions</em></a> <em>were not as safe as I’d like. Until now.</em></p>\n<p>It’s no secret that raw SIMD intrinsics are unpleasant to use.</p>\n<p>You want to write <code>a + b</code>, not this monstrosity:</p>\n<pre><code>unsafe {\n    #[cfg(all(any(target_arch = &quot;x86&quot;, target_arch = &quot;x86_64&quot;), target_feature = &quot;avx2&quot;))]\n    _mm256_add_ps(a, b)\n    #[cfg(all(any(target_arch = &quot;x86&quot;, target_arch = &quot;x86_64&quot;), target_feature = &quot;sse&quot;, not(target_feature = &quot;avx2&quot;)))]\n    _mm_add_ps(a, b)\n    #[cfg(all(target_arch = &quot;aarch64&quot;, target_feature = &quot;neon&quot;))]\n    vaddq_f32(a, b)\n}</code></pre>\n<p>Look at it. It’s hideous. And the whole thing is wrapped in <code>unsafe</code>!</p>\n<p>And that’s a simplified example. It still doesn’t handle:</p>\n<ul><li>Other common platforms: AVX-512, 32-bit ARM, WebAssembly* Platforms without SIMD or obscure platforms like RISC-V* Actually loading data like <code>&amp;[f32]</code> into a form that each intrinsic accepts* Selecting the best implementation for the CPU it’s running on</li></ul>\n<p>Luckily, Rust provides <a href=\"https://shnatsel.medium.com/the-state-of-simd-in-rust-in-2025-32c263e5f53d\" rel=\"nofollow ugc noopener\">many SIMD abstractions</a> that handle all of that for you and let you simply write <code>a + b</code>.</p>\n<p>There is just one wrinkle. Inside, they’re still full of <code>unsafe</code>. It wasn’t gone, just hidden. Vast quantities of it lurking just beneath the surface, getting <a href=\"https://github.com/servo/pathfinder/issues/588\" rel=\"nofollow ugc noopener\">screwed</a> <a href=\"https://github.com/wingertge/macerator/issues/31\" rel=\"nofollow ugc noopener\">up</a> <a href=\"https://github.com/sarah-quinones/pulp/issues/28\" rel=\"nofollow ugc noopener\">occasionally</a>.</p>\n<p>Or rather, they were. Until now.</p>\n<h2 id=\"why-do-we-even-need-unsafe\">Why do we even need ‘unsafe’?</h2>\n<p>For the longest time you couldn’t get around wrapping the call to each intrinsic function such as <code>_mm256_add_ps</code> into <code>unsafe</code> because it is illegal to call one when it’s not available on the CPU you’re running on.</p>\n<p>So you <em>had</em> to have some mechanism for tracking which instructions are needed for each intrinsic, and which instructions you have access to, and cross-referencing them to decide if it’s safe to call a given function.</p>\n<p>It was either tedious if done by hand or complex if done by a code generator, always error-prone, and required <code>unsafe</code> around every intrinsic.</p>\n<p>This changed in Rust 1.87 when the compiler started tracking the required instruction sets itself, so you could write this:</p>\n<pre><code>#[target_feature(enable = &quot;avx2&quot;)]\nfn add_avx2(a: __m256, b: __m256) -&gt; __m256 {\n    _mm256_add_ps(a, b) // this is an avx2 intrinsic\n}</code></pre>\n<p>Look ma, no <code>unsafe</code>!</p>\n<p>…yet.</p>\n<p>You still cannot write <code>a + b</code> with this. The best you can do is this:</p>\n<pre><code>unsafe { add_avx2(a, b) }</code></pre>\n<p>This only shifts the <code>unsafe</code> up a layer. You can call intrinsics inside functions annotated with the correct <code>#[target_feature]</code> now, but there still has to be <code>unsafe</code> somewhere in the chain.</p>\n<p>The other problem is more fundamental. You cannot put <code>#[target_feature]</code> on the implementation of <code>+</code> for your type, because <code>+</code> must be available always. So no <code>a + b</code> for us using this mechanism.</p>\n<h2 id=\"lemma-cpu-feature-tokens\">Lemma: CPU feature tokens</h2>\n<p>To understand how the final solution works, you first need to understand how CPU feature detection works.</p>\n<p>Normally, checking for a CPU feature like AVX2 is done at runtime using <code>is_x86_feature_detected!(&quot;avx2&quot;)</code>. But we definitely don’t want to run this check every single time we add two numbers together — that would completely tank performance. We want to check it <em>once</em>, and then prove to the compiler that it’s safe to use AVX2 instructions from that point on.</p>\n<p>Instead we can encode this proof into the type system using an <em>unforgeable token</em>: a zero-sized type with a private inner field. The <em>only</em> way to obtain this token is to call a function that performs the CPU feature check. If the check passes, the function hands you the token:</p>\n<pre><code>pub struct Avx2(());\n\nfn detect_avx2() -&gt; Option&lt;Avx2&gt; {\n    if is_x86_feature_detected!(&quot;avx2&quot;) {\n        Some(Avx2(()))\n    } else {\n        None\n    }\n}</code></pre>\n<p>And because it’s a zero-sized type, passing this token around has no runtime overhead. It exists purely as a compile-time proof.</p>\n<p>The upshot is that as long as you have an instance of the <code>Avx2</code> struct, you can be sure that AVX2 instructions are available on the system.</p>\n<h2 id=\"the-key-insight\">The key insight</h2>\n<p>The compiler doesn’t know it, but this function is safe to call:</p>\n<pre><code>#[target_feature(enable = &quot;avx2&quot;)]\nfn add_avx2(token: Avx2, a: __m256, b: __m256) -&gt; __m256 {\n    _mm256_add_ps(a, b)\n}</code></pre>\n<p>You can <em>only</em> call this function if you have an <code>Avx2</code> token, which you can only get if AVX2 instructions are available on the system.</p>\n<p>If we can explain to the compiler that this is valid (using <code>unsafe</code>), we can write that <code>unsafe</code> only once and reuse it everywhere.</p>\n<p>What we need is a macro which is safe to invoke:</p>\n<pre><code>with_avx2!(\n    fn add_avx2(token: Avx2, a: __m256, b: __m256) -&gt; __m256 {\n        _mm256_add_ps(a, b)\n    }\n)</code></pre>\n<p>but expands into this behind the scenes:</p>\n<pre><code>fn add_avx2(token: Avx2, a: __m256, b: __m256) -&gt; __m256 {\n    // SAFETY: Avx2 is available according to the token,\n    // and we verified that the inner function is not an `unsafe fn`\n    unsafe { inner(token, a, b) }\n\n    #[target_feature(enable = &quot;avx2&quot;)]\n    fn inner(token: Avx2, a: __m256, b: __m256) -&gt; __m256 {\n        _mm256_add_ps(a, b)\n    }\n}</code></pre>\n<p>Now if you use an intrinsic that isn’t in AVX2, <strong>the compiler will reject it!</strong></p>\n<p>We’ve just managed to provide a safe programming interface to SIMD intrinsics without any bespoke tracking of target features!</p>\n<p>Even though there is one <code>unsafe</code> block still inside it, it’s encapsulated in a sound API, so you cannot misuse it to cause memory safety bugs. In that sense it is just like <code>println!</code>, safely abstracting unsafe code.</p>\n<p>This way you only ever need to review and audit <em>this one macro,</em> not hundreds upon hundreds of bespoke <code>unsafe</code> blocks. And the only things we could <em>possibly</em> screw up in the implementation are:</p>\n<ol><li>Mapping the token to the wrong <code>#[target_feature]</code>- Allowing an <code>unsafe fn</code> to be called from a safe context</li></ol>\n<p>And both failure modes are quite easy to check for.</p>\n<p>So now we can call <code>add_avx2(token, a, b)</code> without <code>unsafe</code>, but that still doesn’t get us to <code>a + b</code>. How do we solve <em>that?</em></p>\n<h2 id=\"generics-to-the-rescue\">Generics to the rescue</h2>\n<p>We cannot annotate the implementation of <code>a + b</code> with <code>#[target_feature]</code> because it must be safe to call from anywhere. And we cannot pass a token into the function because it accepts <code>a</code> and <code>b</code> but not <code>token</code>.</p>\n<p>But even if we could do that, it would make for a pretty ugly API. We want <code>a + b</code> to always work and automatically use the best SIMD instructions without the user ever fussing with tokens.</p>\n<p>We can use generics to solve both problems at once: by defining an <code>f32x8</code> type that’s generic over the available instruction sets, we can implement addition on it that both smuggles a token inside it <em>and</em> creates a separate implementation for each SIMD instruction set!</p>\n<p>This is what it looks like:</p>\n<pre><code>pub trait Level {}\n\n#[derive(Clone, Copy)]\npub struct Avx2(());\nimpl Level for Avx2 {}\n\npub struct f32x8&lt;L: Level&gt; {\n    // For simplicity we&#39;ll back this with an array in this example.\n    // In production code we use native SIMD types for the level.\n    data: [f32; 8],\n    // The smuggled token!\n    token: L,\n}\n\n/// implementation of `a + b` for Avx2\nimpl std::ops::Add for f32x8&lt;Avx2&gt; {\n    type Output = Self;\n\n    fn add(self, rhs: Self) -&gt; Self::Output {\n        // (type conversions abbreviated)\n        // Use the Avx2 token to call our safe wrapper\n        let result = add_avx2(self.token, self, rhs);\n        Self {\n            data: store_m256(result),\n            token: self.token,\n        }\n    }\n}</code></pre>\n<p>And then we can just as easily make it work for any other instruction set, or when SIMD is not available at all:</p>\n<pre><code>#[derive(Clone, Copy)]\npub struct NoSimd(());\nimpl Level for NoSimd {}\n\n/// implementation of `a + b` when no SIMD is available\nimpl std::ops::Add for f32x8&lt;NoSimd&gt; {\n    type Output = Self;\n\n    fn add(self, rhs: Self) -&gt; Self::Output {\n        let result = std::array::from_fn(|i| self.data[i] + rhs.data[i]);\n        Self {\n            data: result,\n            token: self.token,\n        }\n    }\n}</code></pre>\n<p>We’ve just solved safety <em>and</em> runtime instruction selection at once!</p>\n<p>Add a convenience function that gives you the best <code>Level</code> available on the system, and you get pretty much the perfect API for SIMD!</p>\n<h2 id=\"the-abi-would-like-a-word\">The ABI would like a word</h2>\n<p>There is, unfortunately, a fundamental problem to writing <code>a + b</code> and having it lower to SIMD instructions: <strong>function call overhead.</strong></p>\n<p>Calling a function is not free but pretty cheap — just a handful of CPU instructions. But a <em>handful</em> of instructions is a lot more than <em>one</em> instruction we’ve just used for implementing addition!</p>\n<p>So if there’s a function call in the way, addition performance will plummet. And performance is the entire point of using SIMD in the first place!</p>\n<p>The compiler is usually pretty good at erasing this overhead via <a href=\"https://matklad.github.io/2021/07/09/inline-in-rust.html\" rel=\"nofollow ugc noopener\">inlining</a>. It basically copy-pastes the implementation of a function you called into the function calling it, so there’s no more function and no more overhead.</p>\n<p>But <code>#[target_feature]</code> annotations throw a wrench in the works. The compiler cannot inline a function that has a <code>#[target_feature]</code> annotation into one that doesn’t, because the required features are not available in it!</p>\n<p>And guess what cannot have a <code>#[target_feature]</code> annotation? Yeah.</p>\n<p>So how <em>do</em> we make <code>a + b</code> work with SIMD?</p>\n<h2 id=\"inline-it-inline-it-with-fire\">Inline it, inline it with fire!</h2>\n<p>We cannot put <code>#[target_feature]</code> on the function that implements <code>a + b</code>, but we <em>can</em> put it on the function that calls <code>a + b</code>!</p>\n<p>Then we can use <a href=\"https://matklad.github.io/2021/07/09/inline-in-rust.html\" rel=\"nofollow ugc noopener\">inlining</a> to get the implementation of <code>a + b</code> copied into the function calling it, and it ends up in a <code>#[target_feature]</code> context.</p>\n<p>So the call chain looks like this:</p>\n<pre><code>#[target_feature(enable = &quot;avx2&quot;)]\nfn do_stuff() {\n    // TODO: some computation\n    c = a + b;\n    // TODO: some more computation\n}\n\n// which calls into...\n\n#[inline(always)] // Function body will be copied into the caller\nfn add(self, rhs: Self) -&gt; Self::Output {\n    // Use the Avx2 token to call our safe wrapper\n    add_avx2(self.token, self, rhs);\n    // return statement abbreviated\n}\n\n// which calls into...\n\n#[inline] // Function body will be copied into the caller if feasible\n#[target_feature(enable = &quot;avx2&quot;)]\nfn add_avx2(token: Avx2, a: __m256, b: __m256) -&gt; __m256 {\n    _mm256_add_ps(a, b)\n}</code></pre>\n<p>This works.</p>\n<p>You can add these annotations in the right places and abstract over the SIMD levels and you <a href=\"https://docs.rs/fearless_simd/latest/fearless_simd/trait.Simd.html#tymethod.vectorize\" rel=\"nofollow ugc noopener\">don’t even need macros</a>.</p>\n<p>The problem is that whenever you call <code>a + b</code> on SIMD types, you have to do it from a function with either <code>#[inline(always)]</code> or <code>#[target_feature]</code> on it, otherwise the code still compiles but performance plummets.</p>\n<p>Wanna see for yourself? Open <a href=\"https://rust.godbolt.org/z/xo1fEfqnT\" rel=\"nofollow ugc noopener\">this example</a>, remove <code>#[target_feature]</code> from it and watch the generated assembly turn into absolute horror show.</p>\n<p>I’m not sure what can be done about this. The limitation seems quite fundamental for any approach that implements <code>a + b</code> with SIMD.</p>\n<p>The <a href=\"https://shnatsel.medium.com/github.com/rust-lang/rfcs/pull/3525\" rel=\"nofollow ugc noopener\">Struct Target Features RFC</a> solves this for <code>add_avx2(token, a, b)</code> and <code>add::&lt;S: Simd&gt;(token, a, b)</code> but I don’t see a path to <code>a + b</code> just yet.</p>\n<h2 id=\"where-does-that-leave-us\">Where does that leave us?</h2>\n<p>Despite the inlining wrinkle inherent to all SIMD code, we’ve managed to provide a remarkably pleasant SIMD abstraction at a staggeringly low, never-before-seen level of unsafe code.</p>\n<p>You can find the production version of these ideas in <a href=\"https://crates.io/crates/fearless_simd\" rel=\"nofollow ugc noopener\"><code>fearless_simd</code></a> v0.5, <strong>available now</strong> in a package registry near you! And <a href=\"https://github.com/linebender/fearless_simd/blob/d2411665f0726cd6c09bd6fd98af4af18e3c1778/fearless_simd/examples/sigmoid.rs\" rel=\"nofollow ugc noopener\">here’s a small example</a> to see how it all fits together in production.</p>\n<p>The macro used to implement it is also exposed publicly, so you can easily <a href=\"https://github.com/linebender/fearless_simd/blob/e76b820154dc09f41bca8302670424d8ca079219/fearless_simd/examples/srgb.rs\" rel=\"nofollow ugc noopener\">mix and match</a> high-level operations like <code>a + b</code> and platform-specific intrinsics to take full advantage of the hardware.</p>\n<p>There is more than one <code>unsafe</code> block in <code>fearless_simd</code> because it also provides the functionality of <a href=\"https://crates.io/crates/safe_unaligned_simd\" rel=\"nofollow ugc noopener\"><code>safe_unaligned_simd</code></a> crate, but that too is done at a <a href=\"https://github.com/okaneco/safe_unaligned_simd/issues/51\" rel=\"nofollow ugc noopener\">significantly lower</a> amount of unsafe code than the original.</p>\n<p>For me the barrier to using high-level SIMD abstractions was always the sheer amount of <code>unsafe</code> they brought. It was scary and hard to justify.</p>\n<p>But now SIMD in Rust can be truly fearless.</p>\n<h2 id=\"acknowledgements\">Acknowledgements</h2>\n<p>I am shocked that I am the first to put this into production (as far as I can tell), because I’m certainly not the first person to think of this.</p>\n<p>CPU feature tokens are an old and common idea. The <a href=\"https://crates.io/crates/pulp\" rel=\"nofollow ugc noopener\"><code>pulp</code></a> crate has been using them for years, but they relied on handwritten unsafe wrappers around intrinsics, and <a href=\"https://github.com/sarah-quinones/pulp/issues/28\" rel=\"nofollow ugc noopener\">occasionally got them wrong</a>.</p>\n<p>Generating multiple implementations using generics is also an old idea. It is part of the <a href=\"https://linebender.org/blog/towards-fearless-simd/\" rel=\"nofollow ugc noopener\">original <code>fearless_simd</code> concept</a> from 8 years ago. The <a href=\"https://crates.io/crates/simdeez\" rel=\"nofollow ugc noopener\"><code>simdeez</code></a> crate, which predates it, also <a href=\"https://docs.rs/simdeez/1.0.8/simdeez/\" rel=\"nofollow ugc noopener\">seems to use something similar</a>.</p>\n<p>The key insight of combining tokens with a single safe wrapper that delegates to <code>rustc</code> is not unique to me either. Just in the context of <code>fearless_simd</code> crate, Raph Levien has <a href=\"https://github.com/linebender/fearless_simd/commit/81b6ab4c8dd6f25064f539c49818c45c2f686815\" rel=\"nofollow ugc noopener\">experimented with it</a>, and <a href=\"https://github.com/DJMcNab\" rel=\"nofollow ugc noopener\">Daniel McNab</a> created a <a href=\"https://github.com/linebender/fearless_simd/pull/108\" rel=\"nofollow ugc noopener\">more elaborate implementation</a> than mine months ago.</p>\n<p>Daniel’s approach allows fine-grained tracking of every individual CPU feature, as opposed to a handful of fixed CPU feature levels that <code>fearless_simd</code> uses. It is more expressive, but came at the cost of complexity, and his approach never got merged because no other maintainer stepped up to review it. I still hope it will be published as a standalone crate someday.</p>\n<p>Thanks to Daniel and to <a href=\"https://github.com/LaurenzV\" rel=\"nofollow ugc noopener\">Laurenz Stampfl</a> for reviewing all my PRs to <code>fearless_simd</code>, they were big and the quick reviews are really appreciated!</p>\n<p><em>This article was originally published <a href=\"https://shnatsel.medium.com/safe-simd-in-rust-even-on-the-inside-c6f1ff381828\" rel=\"nofollow ugc noopener\">on my Medium</a>.</em></p>","headings":[{"level":2,"text":"Why do we even need ‘unsafe’?","id":"why-do-we-even-need-unsafe"},{"level":2,"text":"Lemma: CPU feature tokens","id":"lemma-cpu-feature-tokens"},{"level":2,"text":"The key insight","id":"the-key-insight"},{"level":2,"text":"Generics to the rescue","id":"generics-to-the-rescue"},{"level":2,"text":"The ABI would like a word","id":"the-abi-would-like-a-word"},{"level":2,"text":"Inline it, inline it with fire!","id":"inline-it-inline-it-with-fire"},{"level":2,"text":"Where does that leave us?","id":"where-does-that-leave-us"},{"level":2,"text":"Acknowledgements","id":"acknowledgements"}]}}