Introduction
Single Instruction, Multiple Data (SIMD) allows a single CPU instruction to operate on multiple values simultaneously. Rust provides access to SIMD through std::arch intrinsics and the #[target_feature] attribute. Understanding these is essential for writing high-performance code that takes advantage of modern CPU capabilities.
Key Concepts
- SIMD: Processing multiple data elements with a single instruction (e.g., adding 4 floats at once).
std::arch: Module providing architecture-specific intrinsics as safe wrappers around CPU instructions.#[target_feature(enable = "...")]: Attribute that enables specific CPU features for a function. The function becomesunsafeto call because calling it on a CPU without the feature is UB.- Runtime feature detection:
is_x86_feature_detected!checks at runtime whether the CPU supports a feature.
Real World Context
Image processing, audio codecs, cryptography, game physics, and machine learning all use SIMD for 2-10x speedups. Rust's standard library uses SIMD internally for str::contains, [u8]::contains, and hash map probing.
Deep Dive
Using std::arch Intrinsics
The std::arch module provides typed wrappers for CPU instructions:
rust#[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; #[target_feature(enable = "avx2")] unsafe fn sum_avx2(data: &[f32]) -> f32 { let mut sum = _mm256_setzero_ps(); // 8 floats = 0.0 for chunk in data.chunks_exact(8) { let v = _mm256_loadu_ps(chunk.as_ptr()); sum = _mm256_add_ps(sum, v); // Add 8 floats at once } // Horizontal sum of the 8 lanes let mut result = [0.0f32; 8]; _mm256_storeu_ps(result.as_mut_ptr(), sum); result.iter().sum() }
The #[target_feature(enable = "avx2")] enables AVX2 instructions for this function. The function is unsafe because calling it on a CPU without AVX2 is undefined behavior.
Runtime Feature Detection
Use runtime checks to safely dispatch to SIMD code:
rustfn fast_sum(data: &[f32]) -> f32 { #[cfg(target_arch = "x86_64")] { if is_x86_feature_detected!("avx2") { // SAFETY: We just checked that AVX2 is available return unsafe { sum_avx2(data) }; } } // Fallback: scalar implementation data.iter().sum() }
The is_x86_feature_detected! macro queries CPUID at runtime. The result is cached after the first call.
Portable SIMD (Nightly)
The std::simd module (nightly) provides portable SIMD that works across architectures:
rust#![feature(portable_simd)] use std::simd::prelude::*; fn add_arrays(a: &[f32; 4], b: &[f32; 4]) -> [f32; 4] { let va = f32x4::from_array(*a); let vb = f32x4::from_array(*b); (va + vb).to_array() }
Portable SIMD compiles to the best available instructions on any platform.
Common Pitfalls
- Forgetting runtime feature detection — Calling a
#[target_feature]function on a CPU that lacks the feature is UB. Always check withis_x86_feature_detected!. - Ignoring alignment — Some SIMD load instructions require aligned pointers. Use
_mm256_loadu_ps(unaligned) instead of_mm256_load_ps(requires 32-byte alignment) unless you've ensured alignment. - Not providing a scalar fallback — Your code must work on CPUs without SIMD. Always provide a non-SIMD fallback path.
Best Practices
- Use
is_x86_feature_detected!for dynamic dispatch — Check once, cache the function pointer for repeated calls. - Benchmark both SIMD and scalar — For small data, the overhead of SIMD setup may exceed the benefit.
- Prefer portable SIMD when available — It works across architectures without architecture-specific code.
Summary
- SIMD processes multiple data elements with a single instruction.
std::archprovides typed intrinsics;#[target_feature]enables them.- Always use runtime feature detection before calling SIMD functions.
- Provide scalar fallbacks for CPUs without SIMD support.
- Portable SIMD (nightly) provides cross-architecture SIMD.
Code Examples
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
/// Count bytes equal to `needle` in `haystack` using SIMD.
/// Falls back to scalar if SSE2 is not available.
fn count_byte(haystack: &[u8], needle: u8) -> usize {
#[cfg(target_arch = "x86_64")]
{
if is_x86_feature_detected!("sse2") {
// SAFETY: SSE2 is available
return unsafe { count_byte_sse2(haystack, needle) };
}
}
// Scalar fallback
haystack.iter().filter(|&&b| b == needle).count()
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "sse2")]
unsafe fn count_byte_sse2(haystack: &[u8], needle: u8) -> usize {
let needle_v = _mm_set1_epi8(needle as i8); // Broadcast needle to 16 lanes
let mut count = 0usize;
for chunk in haystack.chunks_exact(16) {
let data = _mm_loadu_si128(chunk.as_ptr() as *const __m128i);
let cmp = _mm_cmpeq_epi8(data, needle_v); // Compare 16 bytes at once
let mask = _mm_movemask_epi8(cmp) as u32;
count += mask.count_ones() as usize;
}
// Handle remaining bytes
let remainder = haystack.len() % 16;
if remainder > 0 {
let tail = &haystack[haystack.len() - remainder..];
count += tail.iter().filter(|&&b| b == needle).count();
}
count
}