Rust 125 🦀 SIMD Vectorization (std::arch and wide crates)
Single Instruction, Multiple Data (SIMD) allows a CPU to process multiple values with a single instruction. On modern x86_64 processors, this means adding four f32 values in the time it takes to add one, or processing eight u8 values in a single operation. Rust provides two primary paths to SIMD: the std::arch module, which exposes raw, architecture-specific intrinsics with maximum control, and third-party crates like wide, which provide portable vector types that compile down to the appropriate SIMD instructions for the target architecture. The choice between them is a trade-off between raw power and portability, safety, and development speed.
Key point: std::arch is unsafe and architecture-specific. wide is safe, portable, and stable, but lacks runtime CPU feature detection. Choosing the right tool depends on whether you need maximum performance on a known CPU or a portable, maintainable implementation that runs across architectures.
Why SIMD vectorization exists
The throughput problem. Modern CPUs have SIMD registers that are 128, 256, or 512 bits wide. A scalar loop that processes one f32 per iteration uses only 32 bits of a 256-bit register. SIMD fills the remaining bits with additional data, allowing a single instruction to do four or eight times the work. For data-parallel workloads—image processing, cryptography, numerical simulations, string parsing—this is the primary mechanism for extracting performance from the hardware .
The intrinsics problem. The CPU exposes SIMD through specific instructions: addps for SSE, vaddps for AVX, fmla for ARM NEON. Each has a specific encoding, register class, and alignment requirement. Writing assembly by hand is error-prone and architecture-specific. std::arch provides Rust functions that map directly to these instructions, allowing the compiler to handle register allocation and scheduling while the programmer specifies the exact operation .
The portability problem. Code written with std::arch::x86_64::_mm256_add_ps runs only on x86_64 with AVX support. The same algorithm on ARM requires vaddq_f32, and on a CPU without AVX, the x86 code crashes with an illegal instruction. The wide crate abstracts over these differences: f32x8 uses AVX on x86_64 and NEON on aarch64, falling back to scalar operations on unsupported targets. The same code compiles and runs everywhere, though performance varies .
The safety problem. std::arch intrinsics are unsafe because calling an AVX2 intrinsic on a CPU without AVX2 is undefined behavior. The programmer must verify CPU support at compile time (#[cfg(target_feature = "avx2")]) or runtime (is_x86_feature_detected!("avx2")). The wide crate handles this internally by only using instructions that the target guarantees, but it cannot dynamically dispatch to AVX2 on a build that targets only SSE2 .
The trade-off. std::arch gives maximum control and can exploit every CPU feature, but requires unsafe code, runtime dispatch logic, and architecture-specific implementations. wide is safe and portable but lacks runtime dispatch and may underperform on CPUs that support wider instructions than the build target.
a. std::arch: raw intrinsics and CPU feature detection
The std::arch module exposes architecture-named submodules (x86_64, aarch64, wasm32) containing intrinsics that map to single machine instructions . For AVX2 on x86_64, the _mm256_* functions operate on 256-bit vectors represented as __m256 types.
use std::arch::x86_64::*;
#[target_feature(enable = "avx2")]
unsafe fn add_avx2(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
let va = _mm256_loadu_ps(a.as_ptr());
let vb = _mm256_loadu_ps(b.as_ptr());
let vc = _mm256_add_ps(va, vb);
let mut result = [0.0f32; 8];
_mm256_storeu_ps(result.as_mut_ptr(), vc);
result
}
The #[target_feature(enable = "avx2")] attribute informs the compiler that the function requires AVX2. The function remains unsafe because calling it on a CPU without AVX2 is undefined behavior. The _mm256_loadu_ps intrinsic loads 256 bits from an unaligned pointer, avoiding the alignment requirement that _mm256_load_ps imposes.
Runtime dispatch is the mechanism for shipping a portable binary that uses the best available SIMD at runtime. The is_x86_feature_detected! macro checks CPU support once, and a function pointer selects the appropriate implementation.
fn add(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
if is_x86_feature_detected!("avx2") {
unsafe { add_avx2(a, b) }
} else {
// scalar fallback
let mut result = [0.0; 8];
for i in 0..8 { result[i] = a[i] + b[i]; }
result
}
}
The dispatch check should be cached if it is on a hot path. The std::arch approach requires a separate implementation for each architecture and feature level, but it is the only way to guarantee that the binary uses the widest SIMD available on the machine it runs on .
b. wide: portable vector types
The wide crate provides types like f32x4, f32x8, i32x4, and u8x16 that represent SIMD vectors. The API mirrors std::simd—the unstable portable SIMD API in the standard library—making it a near drop-in replacement for code that intends to migrate to std::simd when it stabilizes .
use wide::f32x8;
fn add_wide(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
let va = f32x8::from(*a);
let vb = f32x8::from(*b);
let vc = va + vb;
vc.to_array()
}
The f32x8 type uses AVX on x86_64 when the build enables it, NEON on aarch64, and scalar operations elsewhere. The critical limitation is that wide does not perform runtime CPU feature detection. On x86, the baseline target guarantees only SSE2 (128-bit). To use AVX (256-bit), the build must set RUSTFLAGS="-C target-cpu=native" or explicitly enable the feature. A binary built without AVX will not use f32x8‘s full width even on an AVX-capable CPU .
The wide crate is appropriate when the build target is known and consistent—for example, a binary distributed to a fleet of identical machines, or a library that documents its CPU requirements. It is safe, stable, and significantly easier to read than raw intrinsics, but it cannot adapt to the CPU at runtime .
c. Choosing between them (and what about std::simd?)
The std::simd module is the standard library’s portable SIMD API. It provides types like f32x4 and f32x8 with an API similar to wide, and it is intended to be the long-term solution for portable SIMD in Rust. As of 2026, it remains nightly-only and requires the #![feature(portable_simd)] attribute . The API is not stable and cannot be used in libraries that must compile on stable Rust. wide is the stable alternative for teams that need portable SIMD today but want to migrate to std::simd later .
The choice between std::arch and wide depends on three factors. If the code must run on a heterogeneous fleet of CPUs and use the widest SIMD available on each, std::arch with runtime dispatch is the only option. If the code targets a known architecture and the build flags are controlled, wide is safer and more maintainable. If the code is experimental and can use nightly, std::simd offers a clean, portable API that will eventually stabilize .
Complete Example Session
# ============================================
# PART 1: SCALAR BASELINE
# ============================================
// Process four f32 values at a time, scalar.
fn add_scalar(a: &[f32; 4], b: &[f32; 4]) -> [f32; 4] {
let mut result = [0.0; 4];
for i in 0..4 {
result[i] = a[i] + b[i];
}
result
}
# ============================================
# PART 2: STD::ARCH WITH AVX2
# ============================================
use std::arch::x86_64::*;
#[target_feature(enable = "avx2")]
unsafe fn add_avx2(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
let va = _mm256_loadu_ps(a.as_ptr());
let vb = _mm256_loadu_ps(b.as_ptr());
let vc = _mm256_add_ps(va, vb);
let mut result = [0.0f32; 8];
_mm256_storeu_ps(result.as_mut_ptr(), vc);
result
}
# ============================================
# PART 3: RUNTIME DISPATCH
# ============================================
fn add_dispatch(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
if is_x86_feature_detected!("avx2") {
unsafe { add_avx2(a, b) }
} else {
// scalar fallback
let mut r = [0.0; 8];
for i in 0..8 { r[i] = a[i] + b[i]; }
r
}
}
# ============================================
# PART 4: WIDE CRATE EQUIVALENT
# ============================================
use wide::f32x8;
fn add_wide(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
let va = f32x8::from(*a);
let vb = f32x8::from(*b);
(va + vb).to_array()
}
# ============================================
# PART 5: BUILD FLAGS FOR WIDE
# ============================================
# On x86_64, AVX requires explicit enabling:
RUSTFLAGS='-C target-cpu=native' cargo build --release
# Or:
RUSTFLAGS='-C target-feature=+avx2' cargo build --release
# ============================================
# PART 6: CROSS-PLATFORM WIDE
# ============================================
// On aarch64, f32x4 uses NEON automatically.
use wide::f32x4;
fn dot4(a: &[f32; 4], b: &[f32; 4]) -> f32 {
let va = f32x4::from(*a);
let vb = f32x4::from(*b);
(va * vb).reduce_add()
}
# ============================================
# PART 7: NIGHTLY STD::SIMD
# ============================================
#![feature(portable_simd)]
use std::simd::f32x8;
fn add_simd(a: &[f32; 8], b: &[f32; 8]) -> [f32; 8] {
let va = f32x8::from_array(*a);
let vb = f32x8::from_array(*b);
(va + vb).to_array()
}
# ============================================
# PART 8: BENCHMARK COMPARISON
# ============================================
# Scalar: ~4 cycles for 4 f32 adds
# AVX2 (std::arch): ~1 cycle for 8 f32 adds
# wide f32x8 (AVX2): ~1 cycle, with safe code
# wide without AVX flags: ~4 cycles (SSE2 only)
# ============================================
# PART 9: SAFETY NOTE
# ============================================
// std::arch requires unsafe because CPU support is not
// guaranteed by the type system.
// wide is safe because it only uses instructions the
// target guarantees.
// std::simd is safe but unstable.
The nine parts trace the SIMD landscape: scalar baseline, std::arch AVX2 implementation, runtime dispatch, wide equivalent, build flags, cross-platform wide, nightly std::simd, performance comparison, and the safety distinction.
Quick Reference
std::arch Intrinsics (x86_64 AVX2)
| Intrinsic | Operation |
|---|---|
_mm256_loadu_ps | Load 8 unaligned f32 |
_mm256_add_ps | Add 8 f32 |
_mm256_mul_ps | Multiply 8 f32 |
_mm256_storeu_ps | Store 8 unaligned f32 |
wide Types
| Type | Width | Elements |
|---|---|---|
f32x4 | 128-bit | 4 f32 |
f32x8 | 256-bit | 8 f32 |
i32x4 | 128-bit | 4 i32 |
u8x16 | 128-bit | 16 u8 |
Build Flags for SIMD
| Flag | Effect |
|---|---|
-C target-cpu=native | Enable all CPU features |
-C target-feature=+avx2 | Enable AVX2 only |
RUSTFLAGS | Environment variable for flags |
Best Practices
✅ Do This:
#[target_feature(enable = "avx2")]
unsafe fn avx2_impl() { /* ... */ }
- ✅ Explicitly declare required CPU features
if is_x86_feature_detected!("avx2") { unsafe { avx2_impl() } }
- ✅ Runtime dispatch for portable binaries
use wide::f32x8;
let v = f32x8::from(arr) + f32x8::splat(1.0);
- ✅ Safe, portable SIMD with
wide
❌ Don’t Do This:
unsafe { _mm256_add_ps(a, b) } // on unsupported CPU
- ❌ Undefined behavior if AVX2 is not available
# Use wide without target-cpu flags on x86_64
- ❌
f32x8falls back to SSE2; no AVX
# Use std::simd on stable Rust
- ❌ Requires nightly; not stable as of 2026
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Illegal instruction crash | AVX2 intrinsic on non-AVX2 CPU | Runtime dispatch with is_x86_feature_detected! |
| wide is slow | Build target lacks AVX | Set -C target-cpu=native |
| std::simd compile error | Using stable Rust | Use wide or nightly |
| Alignment fault | _mm256_load_ps on unaligned data | Use _mm256_loadu_ps |
| Different results on different CPUs | Floating-point order differs | Use deterministic reductions |
Real-World Examples
1. AVX2 Addition
unsafe { _mm256_add_ps(a, b) }
2. Runtime Check
if is_x86_feature_detected!("avx2") { /* ... */ }
3. wide f32x8
f32x8::from(arr) * f32x8::splat(2.0)
4. wide Reduction
let sum: f32 = (va * vb).reduce_add();
5. Build for Native CPU
RUSTFLAGS='-C target-cpu=native' cargo build --release
6. Load Unaligned
_mm256_loadu_ps(ptr)
7. Store Unaligned
_mm256_storeu_ps(ptr, vec)
8. NEON f32x4
use wide::f32x4; // uses NEON on aarch64
9. std::simd (Nightly)
#![feature(portable_simd)]
use std::simd::f32x8;
10. Scalar Fallback
for i in 0..len { result[i] = a[i] + b[i]; }
Visual
SIMD Execution Model
┌──────────────────────────────────────────────┐
│ Scalar loop │
│ │
│ [a0] + [b0] → [r0] │
│ [a1] + [b1] → [r1] │
│ [a2] + [b2] → [r2] │
│ [a3] + [b3] → [r3] │
│ │
│ 4 instructions, 4 cycles │
└──────────────────┬───────────────────────────┘
│ SIMD
▼
┌──────────────────────────────────────────────┐
│ SIMD (128-bit) │
│ │
│ [a0|a1|a2|a3] + [b0|b1|b2|b3] → [r0|r1|r2|r3]│
│ │
│ 1 instruction, 1 cycle │
└──────────────────┬───────────────────────────┘
│ AVX2 (256-bit)
▼
┌──────────────────────────────────────────────┐
│ SIMD (256-bit) │
│ │
│ [a0..a7] + [b0..b7] → [r0..r7] │
│ │
│ 1 instruction, 8 f32 values │
└──────────────────────────────────────────────┘
The wider the vector, the more data processed per instruction.
std::arch vs wide Decision
┌──────────────────────────────────────────────┐
│ Need runtime dispatch across CPUs? │
└──────────┬───────────────────────┬───────────┘
│ YES │ NO
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ std::arch │ │ Target CPU known? │
│ + is_x86_feature_ │ └──────────┬───────────┘
│ detected! │ │ YES
│ │ ▼
│ Unsafe, maximum │ ┌──────────────────────┐
│ performance │ │ wide (safe) │
└──────────────────────┘ │ or std::simd (nightly)│
└──────────────────────┘
The choice is about control versus maintainability.
Summary
| Item | Value |
|---|---|
| std::arch stability | Stable, but unsafe |
| std::arch portability | Architecture-specific |
| std::arch dispatch | Manual (is_x86_feature_detected!) |
| wide stability | Stable |
| wide safety | Safe (no unsafe required) |
| wide portability | x86, ARM, WASM |
| wide dispatch | None (build-time features only) |
| std::simd status | Nightly-only (2026) |
| Build flag for AVX | -C target-cpu=native |
Key takeaways:
std::archexposes raw CPU instructions. It is the only way to use every SIMD feature the CPU supports, but it requiresunsafecode and manual runtime dispatch .wideprovides safe, portable SIMD. It compiles to the best available instructions for the target, but cannot dynamically adapt to the CPU at runtime .std::simdis the future, but not yet stable. It offers the cleanest API and will eventually replacewidefor portable SIMD, but as of 2026 it requires nightly Rust .- Build flags matter for
wide. On x86_64,f32x8only uses AVX if the build enables it. Without-C target-cpu=native, it falls back to SSE2 . - Runtime dispatch is essential for portable binaries. A binary built without AVX can still use AVX if the code checks the CPU feature at runtime and dispatches to an AVX-specific function.
Remember: SIMD is a tool for data-parallel loops. std::arch gives you the keys to the hardware at the cost of safety and portability. wide gives you a portable, safe abstraction at the cost of runtime adaptability. Choose based on whether the deployment target is known and consistent, or heterogeneous and changing.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!