Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
236: SIMD in Rust
I've noticed a recurring theme when developers first dive into high-performance Rust: they assume that to use SIMD (Single Instruction, Multiple Data), they have to abandon the comfort of the language and start writing architecture-specific intrinsics like _mm256_add_epi32. They think SIMD is a "dark art" reserved for people who have the Intel 64 and IA-32 Architectures Software Developer's Manual memorized.
The "Intrinsics or Bust" Fallacy
The misconception is that manual, platform-specific assembly or intrinsics are the only way to get a meaningful speedup. In reality, if you write a simple loop to add two slices of numbers, the LLVM backend in Rust is often smart enough to "auto-vectorize" that code. It sees the pattern and emits SIMD instructions for you without you ever asking.
Take this snippet. You might think it's just a standard scalar loop:
fn sum_everything(data: &[i32]) -> i32 {
data.iter().sum()
}
If you compile this with -C target-cpu=native, the compiler doesn't just add one number at a time. It will likely use AVX or SSE registers to add 4, 8, or 16 numbers in a single clock cycle. If you're relying solely on auto-vectorization, you're basically hoping the compiler is smart enough to recognize your intent. Most of the time it is, but for complex logic—like conditional branching inside a loop—the compiler often gives up and falls back to slow, scalar execution.
Leveraging std::simd for Platform-Agnostic Parallelism
This is where std::simd (the portable SIMD project) comes in. Instead of writing code that only works on an x86_64 machine with AVX2, you can use portable SIMD types. I prefer this approach because it keeps the code readable while giving you an explicit guarantee that the operation is vectorized.
Imagine we are processing a raw buffer of image pixels (u8) and we want to increase the brightness of the image by adding a constant value to every pixel. If we do this naively, we'd loop through every single byte. With portable SIMD, we can process chunks of pixels simultaneously.
#![feature(portable_simd)]
use std::simd::u8x32; // A SIMD vector of 32 unsigned 8-bit integers
pub fn brighten_image(pixels: &mut [u8], offset: u8) {
let (prefix, middle, suffix) = pixels.as_simd_mut::<32>();
let offset_vec = u8x32::splat(offset);
// Handle the bulk of the data in 32-byte chunks
for chunk in middle {
*chunk = *chunk + offset_vec;
// Note: this wraps around by default in Rust for u8
}
// We still have to handle the edges manually
for p in prefix.iter_mut().chain(suffix.iter_mut()) {
*p = p.wrapping_add(offset);
}
}
By using u8x32, we've told the compiler: "I want to treat this data as a block of 32 bytes." On a machine with AVX-256, this maps directly to a single instruction. On an ARM machine with NEON, it maps to the equivalent available width. I love this because the intent is explicit, but the implementation remains portable.
Dealing with the "Tail" of Your Data
One thing that often trips people up is the "remainder" problem. SIMD works in fixed widths—say, 32 bytes. But your image buffer might be 1,027 bytes long. You can't fit that last 3 bytes into a 32-byte register without reading past the end of your allocated memory, which would be a catastrophic security flaw or a crash.
In the example above, I used as_simd_mut. This is a lifesaver. It splits your slice into three parts: a prefix (the odd bits at the start), the middle (the perfectly aligned chunks), and a suffix (the leftover bits at the end). Never ignore the prefix and suffix; if you do, you'll either miss data or trigger a panic. I usually handle the "tails" with a simple scalar loop, as the performance hit on a few bytes is negligible compared to the gain on the rest of the buffer.
📋 Practical Task
Implementing a SIMD-Accelerated Saturated Addition Filter
In image processing, a "wrapping add" (where 255 + 1 becomes 0) creates ugly black artifacts. We want saturated addition: if the result is greater than 255, it should stay at 255.
Your task is to modify the brightness function. Instead of using the default + operator (which wraps), use the saturating_add method available on SIMD vectors to ensure pixels never wrap around.
Requirements:
- Use
#![feature(portable_simd)]. - Use
u8x16oru8x32for the vector width. - Implement the logic using
as_simdto handle the bulk of the data. - Ensure the prefix and suffix are also processed using scalar
saturating_add. - Create a test case with a buffer containing
[250, 255, 10, 5]and an offset of10; the result should be[255, 255, 20, 15].
There are no comments for now.