De status van SIMD in Rust in 2026

Sinds vorig jaar is er veel vooruitgang geboekt, en daar heb ik zelf aan bijgedragen!

Na de enquête van vorig jaar ben ik begonnen met het bijdragen aan de SIMD-bibliotheek die het meest veelbelovend leek. Het een leidde tot het ander, en inmiddels ben ik een maintainer van Fearless SIMD.

Om belangenverstrengeling te voorkomen, heb ik auteurs van andere bibliotheken (std::simd, wide, pulp, macerator) uitgenodigd om een concept van dit artikel te beoordelen en feedback te geven. Ik heb echter de redactionele controle behouden, en alle fouten zijn voor mijn eigen rekening.

De enquête van dit jaar is diepgaander dan de vorige. Dus zet je schrap, en we beginnen... bij het begin!

Wat is SIMD? Waarom SIMD?

Hardware die rekenkundige bewerkingen uitvoert is goedkoop, dus elke CPU die in deze eeuw is gemaakt, heeft er in overvloed. Maar je hebt nog steeds maar één instructie-decoderingseenheid en het is lastig om die sneller te laten werken, waardoor de rekenhardware zwaar onderbenut blijft.

Om deze bottleneck bij de instructie-decodering te omzeilen, kun je de CPU in één keer een batch getallen voeren voor een enkele rekenkundige bewerking, zoals een optelling. Vandaar de naam: "single instruction, multiple data", of SIMD.

In plaats van twee getallen bij elkaar op te tellen, kun je twee batches of "vectoren" van getallen optellen, wat ongeveer dezelfde tijd kost als één enkele optelling.

Op recente x86-chips kunnen deze batches tot 512 bits groot zijn. In theorie kun je dus een 8x versnelling krijgen voor berekeningen met f64 of een 64x versnelling voor u8. In de praktijk kan het zowel langzamer als sneller lopen.

Instructiesets

Historisch gezien werden SIMD-instructies toegevoegd nadat de CPU-architectuur al was ontworpen. SIMD is dus een extensie met een eigen marketingnaam per architectuur.

  • ARM noemt de hunne "NEON", en alle 64-bit ARM-CPU's hebben dit.
  • WebAssembly heeft geen marketingafdeling, dus zij noemen het simpelweg de "WebAssembly 128-bit packed SIMD extension".
  • 64-bit x86 werd geleverd met "SSE2", dat basisinstructies heeft voor 128-bit vectoren. Later werden daar een hele reeks extensies aan toegevoegd: SSE 4.2 voegde meer operaties toe, AVX en AVX2 voegden 256-bit vectoren toe, en AVX-512 voegde 512-bit vectoren en nog meer operaties toe.

Het woord "later" in de bovenstaande paragraaf veroorzaakt een probleem.

Beschikt deze CPU over die instructie?

Als je een programma draait op een x8664 CPU, is het niet gegarandeerd dat de CPU over een specifieke SIMD-extensie beschikt. Standaard mag de compiler daarom geen instructies gebruiken die verder gaan dan SSE2, omdat deze niet op alle x8664 CPU's werken.

Er zijn twee manieren om dit probleem op te lossen.

Ten eerste: als je werkt voor een bedrijf dat binaries alleen op eigen servers of in een public cloud draait, kun je simpelweg aannemen dat deze recent genoeg zijn om ten minste AVX2 te hebben (geïntroduceerd meer dan 10 jaar geleden). Je kunt het programma dan laten crashen of fouten vertonen als het op hardware zonder AVX2 draait:

RUSTFLAGS='-C target-cpu=x86-64-v3' cargo build --release

Als je echter binaries distribueert aan anderen, is dit geen optie. In dat geval kun je gebruikmaken van function multiversioning: compileer dezelfde functie meerdere keren voor verschillende SIMD-extensies. Wanneer het programma daadwerkelijk wordt uitgevoerd, controleert het welke functies de CPU ondersteunt en selecteert het de juiste versie.

Gelukkig bestaat dit probleem alleen op x86. ARM heeft NEON verplicht gesteld op 64-bit CPU's en heeft daarna geen echt nuttige SIMD-extensies toegevoegd. WebAssembly vereist dat je twee verschillende binaries compileert (één met SIMD en één zonder) en JavaScript gebruikt om te controleren of de browser SIMD ondersteunt.

Hoe pas ik SIMD toe?

Er zijn drie manieren om SIMD te benutten:

  1. Automatische vectorisatie: &[i32].sum()
  2. Portable SIMD-abstracties: i32x4 + i32x4
  3. Platformspecifieke intrinsics (hiervoor is een groter codeblok nodig):
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), target_feature = "sse2"))]
_mm_add_epi32(__m128i, __m128i)

#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
vaddq_u32(int32x4_t, int32x4_t)

Laten we kijken naar wat elke methode inhoudt en wat de huidige status is.

Automatische vectorisatie

Schrijf gewoon normale Rust-code en laat de heuristiek van de compiler het werk doen!

Dit kan erg goed werken als je zorgvuldig code schrijft op een manier die de compiler betrouwbaar(ish) kan vectoriseren. Dit houdt meestal in dat je itereert over &[i32].as_chunks() in plaats van &[i32] en dat je benchmarks uitvoert of naar de assembly kijkt om te verifiëren of het gewerkt heeft.

Het grootste voordeel is dat dit de makkelijkste optie is, geen dependencies vereist en automatisch alle instructiesets ondersteunt die de compiler ondersteunt.

Het nadeel is dat deze methode onbetrouwbaar is. Hoe groter en complexer je functie, hoe groter de kans dat de compiler deze niet kan vectoriseren. De prestaties kunnen ook sterk schommelen afhankelijk van de compilerversie of wijzigingen in de omringende code.

Floating-point types vereisen speciale aandacht. Floats zijn vreemd; zelfs het optellen van een array van floats met redelijke precisie is verrassend complex. Voorheen werkte automatische vectorisatie niet met floating-point types omdat dit de precisie van het resultaat zou veranderen (vaak ten goede, maar de compiler mag observeerbare resultaten niet zomaar wijzigen).

Dit veranderde in Rust 1.98, waarin algebraïsche operaties zoals algebraic_add() werden gestabiliseerd. Hiermee kan de compiler het observeerbare resultaat wijzigen, vergelijkbaar met een minder gevaarlijke -ffast-math. In de meeste gevallen moet je je code nog steeds herschrijven om deze functies te gebruiken voordat vectorisatie mogelijk is.

En je hebt nog steeds een manier nodig voor multiversioning.

De multiversion crate

De all-in-one SIMD-crates die hierna worden besproken bieden ook multiversioning, maar laten we kort naar multiversion kijken, aangezien dit het meest nuttig is voor automatische vectorisatie.

Het is zeer eenvoudig in gebruik: voeg de annotatie #[multiversion(targets = "simd")] toe aan je functie en dat is alles. Maar deze eenvoud verbergt een ongedocumenteerde valkuil: het aanroepen van een functie met #[multiversion] brengt een kleine overhead met zich mee (minder dan een dozijn instructies). Dit is significant als de functie zelf erg klein is.

Als vuistregel: als je functie een loop bevat, voeg dan #[multiversion] toe; als hij slechts een handvol waarden verwerkt, voeg dan #[inline(always)] toe, mits er ergens hoger in de call-chain #[multiversion] staat.

De andere crates hebben deze valkuil niet, maar vereisen meer boilerplate. multiversion is de enige crate waarmee je de exacte CPU-extensies kunt opgeven in plaats van te kiezen voor een vooraf gedefinieerd SIMD-niveau.

Voor AVX-512 controleert multiversion of het aanwezig is, niet of het daadwerkelijk snel is, wat de prestaties in de praktijk kan schaden. Je kunt dit omzeilen ten koste van veel boilerplate door dit op elke functie te zetten:

#[multiversion::multiversion(targets(
    "x86_64+cmpxchg16b+popcnt+sse3+sse4.1+sse4.2+ssse3", // x86_64-v2
    "x86_64+avx+avx2+bmi1+bmi2+cmpxchg16b+f16c+fma+lzcnt+movbe+popcnt+sse3+sse4.1+sse4.2+ssse3+xsave", // x86_64-v3
    "x86_64+fxsr,adx,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,gfni,lzcnt,movbe,pclmulqdq,popcnt,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves", // Ice Lake and later
))]]

Portable SIMD-abstracties

Er zijn verschillende productieklaar opties. De gewenste functies zijn:

  • Fixed-width vectors: code schrijven in termen van f32x4, u8x16, etc. (bekende grootte).
  • Hardware-width vectors: gebruikmaken van de grootste vectorgrootte die de hardware ondersteunt, zonder deze vooraf te kennen.
  • Generiek over elementtype: code die werkt op zowel f32x4 als f64x2.
  • Generiek over vectorbreedte: code die werkt op f32x4, f32x8 en f32x16.

Vergelijkingstabel

Featurestd::simd (nightly)fearless simdwidepulpmacerator
Multiversioning📦/🛠️�
❌�
�
Fixed-width vectors�
�
�
☑️❌
Hardware-width vectors📦/🛠️�
🛠️�
�
Generiek over elementtype�
�
🛠️🛠️�
Generiek over vectorbreedte�
�
🛠️�
�
Veilige toegang tot intrinsics🛠️�
☑️�
🛠️
Trigonometrie📦/🛠️🛠️☑️🛠️🛠️
  • �

Ja

  • ☑️ Ja, met kanttekeningen
  • 📦 Ja, met een third-party crate
  • 🛠️ Zelf bouwen
  • ❌ Absoluut niet

Ondersteuning voor instructiesets

Instructiesetstd::simdfearless simdwidepulpmacerator
SSE2�
�
�
☑️🐌
SSE4.x�
�
�
☑️�
AVX2�
�
�
�
�
AVX-512�
�
�
�
�
NEON�
�
�
�
�
WASM�
�
�
�
�
Al het overige�
🐌🐌🐌🐌*
  • �

Heeft geoptimaliseerde routines

  • ☑️ Geïmplementeerd maar niet gebruikt. Vereist custom dispatch.
  • 🐌 Vertrouwt op autovectorisatie, vaak traag.
  • \ macerator ondersteunt ook LoongArch omdat de auteur zich "verveelde".*

std::simd

std::simd is geen complete oplossing, maar eerder een set bouwstenen die in de standaardbibliotheek horen, terwijl de rest aan ecosystem crates wordt overgelaten.

Het grootste nadeel is dat het alleen beschikbaar is in nightly en nog onderhevig is aan infrequent breaking API-wijzigingen. De bestaansreden van std::simd is dat het direct bovenop LLVM zit en elk platform kan targeten dat LLVM ondersteunt. Aan de andere kant: als LLVM geen perfect passende operatie heeft, is er geen plan B en wordt er geen SIMD gebruikt.

Dit gebeurt zorgwekkend vaak. De implementatie van sin() is een goed voorbeeld: het leveren van scalaire implementaties in een SIMD-jasje is een doodzonde. Ook reduce_sum() is een slecht voorbeeld qua prestaties en nauwkeurigheid. Gebruik dus geen niet-triviale functies op floats.

De beste trigonometrie is momenteel te vinden in de sleef crate, een gedeeltelijke poort van SLEEF naar std::simd, al is deze nog wat buggy.

std::simd is uniek flexibel wat betreft multiversioning; je kunt de multiversion crate of multiversioning van andere SIMD-crates gebruiken. De Simd<T, N> API ziet er elegant uit, maar het doen van wiskunde op N is nog zeer onvolledig, zelfs in nightly.

fearless_simd

Fearless SIMD is een all-in-one oplossing waarbij alle onderdelen op elkaar zijn afgestemd.

Unieke kenmerken van fearless_simd zijn:

  • Aanzienlijk minder unsafe code onder de motorkap dankzij een slim ontwerp.
  • Multiversioning die gewoon werkt, zelfs voor kleine functies. Gebruik simpelweg #[simd]. Er is ook een handmatige modus.
  • Multiversioning wordt beheerd door degene die de uiteindelijke binary bouwt; configuratie is mogelijk zonder de libraries te patchen.

Het belangrijkste nadeel is de boilerplate. In plaats van fn myfunc(a: A, b: B), moet je schrijven: #[simd] fn myfunc<S: Simd>(simd: S, a: A, b: B).

AVX-512 wordt alleen gebruikt op recentere CPU's waar het de prestaties niet schaadt. De grootste leemte is trigonometrie; er is nog geen poort van iets als SLEEF naar de fearless_simd machinery.

wide

wide heeft veel voordelen: goede platformdekking, veel geïmplementeerde operaties en het is al v1.0. Het heeft zelfs trigonometrische functies, hoewel de precisie daarvan niet is gespecificeerd.

Het grootste nadeel is dat het fundamenteel incompatibel is met multiversioning. Dit is prima als je geen x86 target of als je altijd bouwt met -C target-cpu= voor bekende hardware, maar anders beperkt het de prestaties. De enige workaround is cargo multivers, maar dat werkt alleen voor programma's die lang draaien.

Een ander nadeel is het gebrek aan generics voor elementtypes of vectorbreedtes. Dit kan worden opgelost met macros (macro_rules!), wat extra boilerplate oplevert maar de noodzaak voor generieke bounds wegneemt.

pulp

pulp is gebouwd om de faer lineaire algebra bibliotheek aan te drijven. De prioriteiten liggen bij wiskundige operaties en de API is gericht op native-width vectoren.

Vectoren met vaste breedte zijn technisch mogelijk, maar ongedocumenteerd en onhandig in gebruik. Er is geen native ondersteuning voor generics over het elementtype, maar de macro-truc van wide werkt hier ook. De multiversioning is de meest verbose die ik ooit heb gezien, zelfs met de beschikbare macro's.

macerator

macerator is een verwant van pulp met een soortgelijk ontwerp, gebouwd voor de CPU-backend van burn.

Vergeleken met pulp voegt het ondersteuning toe voor code die generiek is over het elementtype, maar verwijdert het veilige toegang tot intrinsics en het grootste deel van de documentatie. Er is geen ondersteuning voor fixed-width vectoren.

Het schakelt standaard SSE4.2 in en voegt geoptimaliseerde codepaths toe voor LoongArch. Dit is de enige bibliotheek naast std::simd met portable operaties op f16 data, hoewel de lijst met ondersteunde operaties beperkt is.

---

Veilige toegang tot intrinsics

Portable SIMD is goed, maar soms heb je een specifieke instructie nodig die alleen in een bepaalde instructieset zit. In dat geval moet je intrinsics direct gebruiken.

Vanaf Rust v1.87+ is het mogelijk om platformspecifieke intrinsics veilig aan te roepen:

#[target_feature(enable = "avx2")]
fn add_avx2(a: __m256, b: __m256) -> __m256 {
    _mm256_add_ps(a, b) // dit is een avx2 intrinsic
}

Er zijn twee kanttekeningen:

  1. Intrinsics om data uit het geheugen te laden of op te slaan zijn nog steeds unsafe omdat ze werken met raw pointers.
  2. De functie addavx2 vereist nog steeds een unsafe blok om aangeroepen te worden vanuit een functie die niet is geannoteerd met #[targetfeature(enable = "avx2")].

Er zijn oplossingen voor beide: gebruik veilige wrappers voor loads/stores (die bounds checks toevoegen die door de optimizer worden verwijderd) en controleer runtime of een CPU-feature beschikbaar is via type-level tokens.

Archmage

archmage biedt CPU-feature tokens en gebruikt de safeunalignedsimd crate voor veilige load/store wrappers. Het middelpunt is de #[arcane] procedural macro. Je krijgt een selectie van vooraf gedefinieerde SIMD-niveaus (SSE2, SSE4.2, AVX2 en twee niveaus van AVX-512). Er is geen ondersteuning voor 32-bit x86.

Fearless SIMD

fearless_simd biedt via zijn kernel! macro vrijwel dezelfde tools als archmage. Dit is een declaratieve macro, wat de build-tijden verbetert, maar het ondersteunt geen annotaties op generieke of const-generieke functies.

Je kunt portable SIMD en intrinsics eenvoudig mengen. Dit stelt je in staat om het grootste deel van het algoritme in portable SIMD te schrijven en alleen intrinsics te gebruiken waar dat echt nodig is.

Pulp

pulp is verrassend krachtig in dit opzicht. Als je intrinsics wilt gebruiken die geen deel uitmaken van een standaard SIMD-niveau (zoals mmaesenc_si128), is dit de beste manier om dit veilig te doen.

Het is echter slecht gedocumenteerd. Als je simpelweg een intrinsic aanroept via een feature-struct, zijn de prestaties slecht vanwege de function call overhead. Je moet in plaats daarvan een context aanmaken met alle vereiste features en vanuit die context de intrinsics aanroepen:

pulp::simd_type! {
    pub struct Ifma {
        pub ifma: "avx512ifma",
    }
}

if let Some(isa) = Ifma::try_new() {
    isa.vectorize(
        #[inline(always)]
        || {
            // Plaats hier de volledige hot loop.
            // isa.ifma._mm512_madd52lo_epu64(...)
        },
    );
}

---

De status van intrinsics

SIMD-intrinsics vormen de basis van bijna alle SIMD-code. Men zou verwachten dat ze perfect werken, maar dat is niet het geval in Rust, C++ of C.

Er is een fundamentele spanning tussen "geef me exact deze instructie" en compiler-optimalisaties. Als de compiler intrinsics als pure black boxes behandelt, ontstaan er inefficiënties.

Een voorbeeld op ARM was dat u32x4::from([1,2,3,4]) traag was. De onderliggende intrinsic vld1u32x4 was geïmplementeerd als een black-box operatie. De gegenereerde assembly laadde de constante in registers, plaatste deze op de stack, en laadde hem vervolgens weer terug via de intrinsic. De oplossing was om de black-box implementatie te verwijderen en er een compatibiliteitswrapper van te maken die de compiler kan optimaliseren.

SIMD-intrinsics zitten vast in een voortdurend touwtrekken tussen het genereren van de verwachte instructies en het samenwerken met compiler-optimalisaties. Daarnaast zijn er bugs in de instructieselectie van de compiler. Ik heb LLVM-bugs gevonden waarbij een 512-bit vector shuffle werd "geoptimaliseerd" naar SSE4.2-operaties, wat veel trager was.

Dit is niet alleen een LLVM-probleem; GCC heeft soortgelijke problemen en MSVC is merkbaar slechter dan beide open-source compilers. Een leuk feitje: Intel vergat sommige AVX-512 instructies in hun Intrinsics Guide op te nemen, waardoor de meeste compilers ze niet hebben geïmplementeerd.

Inline assembly

Je zou denken dat je de compiler kunt slim zijn met inline assembly, maar dan wordt je assembly-blok een echte black box. Niet alleen komen alle optimalisatieproblemen terug, maar het betreden en verlaten van het blok brengt overhead met zich mee. Dit is alleen rendabel voor grotere functies.

---

Wishlist voor compiler-functies

In volgorde van belangrijkheid:

  1. mingenericconst_args: zou het gebruik van arrays in combinatie met hardware-width SIMD-vectoren mogelijk maken.
  2. Struct Target Features RFC: zou betekenen dat fearless_simd/pulp/macerator geen #[simd] annotaties meer nodig hebben, wat boilerplate vermindert en fouten voorkomt.
  3. Een manier om iterators te gebruiken zonder dat deze conflicteren met multiversioning (ook oplosbaar via de Struct Target Features RFC).
  4. genericconstargs (niet min): zou de build-tijden aanzienlijk verbeteren bij het wrappen van intrinsics en std::simd prettiger maken.
  5. In de standaardbibliotheek: crater-achtige verificatie van wijzigingen in intrinsics en std::simd beschikbaar maken in stable.

Conclusie

De ondersteuning voor SIMD in het Rust-ecosysteem is sterk volwassen geworden. Hoewel sommige zaken nog verbeterd kunnen worden (met name trigonometrie), maken recente vorderingen in compiler-functies en bibliotheken Rust aantrekkelijk voor SIMD-code, zelfs wanneer geheugenveiligheid geen harde eis is.

---

Bonusronde: De status van de hardware

x86

Het is een chaos, en we kunnen Intel bedanken.

AVX2 is tegelijkertijd de meest voorkomende en de meest vervloekte instructieset. Vaak hebben AVX-512 en NEON een operatie native, terwijl AVX2 complexe en trage emulatie nodig heeft. Dit komt omdat AVX2 eigenlijk twee 128-bit eenheden is die tegen elkaar aan zijn geplakt.

Bovendien was er tot 2021 nog steeds Intel-hardware zonder AVX2. In 2026 heeft volgens de Firefox hardware survey nog steeds 15% van de x86-CPU's geen AVX2.

AVX-512 was een grote verbetering, maar Intel plaatste het aanvankelijk alleen in servers. Bovendien zorgde het gebruik van AVX-512 op vroege Intel-CPU's voor een verlaging van de kloksnelheid (downclocking) van alle cores. Dit werd pas in 2019 (Ice Lake) opgelost. Daarom ondersteunt fearless_simd AVX-512 alleen op Ice Lake en later, en op AMD (die nooit downclocking-problemen had).

AMD heeft laten zien hoe slecht Intel het had aangepakt met Zen 4, dat geen native 512-bit operaties had maar toch Intel's native hardware versloeg in benchmarks. Zen 5 met native 512-bit hardware heeft dit definitief bezegeld.

Een ander probleem is dat AMD's scatter/gather instructies niet native zijn geïmplementeerd en vaak trager zijn dan meerdere kleine loads. LLVM genereert soms scatter/gather voor AVX-512, wat de prestaties op Intel met 1.75x en op AMD met 4x verslechtert.

ARM

Alles wat AVX2 fout deed, doet 64-bit NEON goed. Met slechts 128-bit vectoren lijkt het op SSE4.2, maar het presteert eerder als AVX2. NEON heeft dezelfde registerruimte als AVX2 en krachtige ARM-cores voeren 128-bit operaties transparant parallel uit via instruction-level parallelism.

In mijn tests met base64-decodering is hetzelfde algoritme 1.5x tot 2x sneller op NEON dan op AVX2. Bovendien is het een simpel programmeermodel dat verplicht is in 64-bit ARM-chips, waardoor multiversioning niet nodig is.

De enige kritiek is het verschil tussen "performance" en "efficiency" cores; een sequentie die snel is op de ene, kan traag zijn op de andere. SVE is momenteel vrijwel nutteloos; SVE2 is wel verplicht, maar wordt vaak op 128-bit breedte geïmplementeerd, waardoor het een onhandige versie van NEON is.

RISC-V

RISC-V vectoren (RVV) zijn momenteel irrelevant omdat vector-capabele RISC-V hardware in 2026 een slechte prijs-prestatieverhouding heeft. Zelfs als er goede hardware zou zijn, maakt de manier waarop de spec is geschreven bepaalde cruciale instructies onbruikbaar voor compilers.

Overige

De overige SIMD-architecturen zijn zo obscuur dat je er geen rekening mee hoeft te houden, tenzij je ervoor betaald wordt (bijv. IBM/banken voor POWER en s390x, of de Chinese overheid voor LoongArch). std::simd draait nog steeds CI op 64-bit SPARC, maar niemand gaat je betalen om dat te ondersteunen.