Case-Folding at 45 GiB/s: Branchless Byte-Space Arithmetic in Code Search
When you build a code search engine operating at petabyte scale, every clock cycle spent on text normalization acts as a direct tax on throughput and infrastructure costs. For years, case-insensitive search has relied on standard string casing routines—routines that look harmless until you inspect them under a CPU profiler processing millions of files per second.
Recently, engineering posts detailed how modern code search engines achieve case-folding speeds exceeding 45 GiB/s on a single CPU core. At that scale, string processing is no longer CPU-bound; it saturates modern hardware memory bandwidth limits.
How do you break past the 10 to 15 GiB/s ceiling of traditional SIMD vectorization? The secret lies in abandoning conditional branches entirely and leveraging byte-space arithmetic across raw byte vectors.
The Problem with Standard Case-Folding
Standard case-folding algorithms (like C's tolower() or Rust's to_lowercase()) operate with conditional branching or character-by-character decoding logic. A typical implementation checks if a byte falls within the uppercase ASCII range (65 to 90, or 'A' to 'Z') and adds 32 (0x20) to convert it to lowercase.
// Naive byte casing with branching
pub fn naive_lowercase_ascii(src: &[u8], dst: &mut [u8]) {
for (i, &byte) in src.iter().enumerate() {
if byte >= b'A' && byte <= b'Z' {
dst[i] = byte + 0x20;
} else {
dst[i] = byte;
}
}
}
Even when auto-vectorized by modern compilers, conditional checks introduce branch mispredictions and vector lane masked stores that severely degrade throughput.
When processing code repositories containing mixed ASCII and UTF-8 content, traditional implementations hit three major performance bottlenecks:
- Branch Misprediction: Source code contains an unpredictable mix of uppercase letters, lowercase letters, punctuation, whitespace, and multi-byte UTF-8 bytes. Branch predictors stall repeatedly.
- Early Stopping Fallbacks: Checking for non-ASCII bytes or special Unicode sequences often forces vectorization pipelines to exit early and drop back to slow scalar loops.
- Memory Bus Starvation: Modern CPUs (such as x86_64 Zen 4 or Apple M-series) can pull data from memory at over 50 GiB/s per core. Scalar logic running at 1.5 GiB/s wastes over 95% of available memory bus bandwidth.
The Byte-Space Arithmetic Technique
In ASCII encoding, the difference between an uppercase letter ('A' = 0x41 / 0b01000001) and a lowercase letter ('a' = 0x61 / 0b01100001) is a single bit: bit 5 (0x20).
Setting bit 5 converts ASCII uppercase letters to lowercase. However, setting bit 5 unconditionally across all bytes ruins non-letter characters. For example:
'1'(0x31) becomes'Q'(0x51).- Punctuation like
'@'(0x40) becomes'`'(0x60).
To transform uppercase ASCII without corrupting other characters, we need a byte mask M where M = 0x20 if the byte is in the range [0x41, 0x5A], and M = 0x00 otherwise. Then we compute:
dst_byte = src_byte | M
The goal is computing M purely through branchless arithmetic.
Constructing the Branchless Range Mask
To test whether a byte x falls within the range [A, B] without conditional branching, we can use unsigned wrapping arithmetic.
For any byte x:
- Subtract
b'A'(0x41) using wrapping subtraction. Bytes smaller thanb'A'wrap around to high values (0x80through0xFF). - Subtract
25(b'Z' - b'A') using saturating subtraction. - Extract the resulting byte status to form a bitmask without any conditional
ifstatements.
Using SIMD vector registers (16, 32, or 64 bytes wide), this operation executes across 512 bits simultaneously using saturating subtraction operations.
Implementation: SIMD Branch-Free Case Folding in Rust
Below is a conceptual implementation demonstrating how byte-space branchless arithmetic operates across 64-byte chunks using vector operations:
#[inline(always)] pub fn case_fold_chunk_512(chunk: &[u8; 64], out: &mut [u8; 64]) { // Process 64 bytes in a single pass without branches for i in 0..64 { let b = chunk[i]; // Wrapping subtraction shifts 'A' to 0 let offset = b.wrapping_sub(b'A'); // Branchless check: yields 1 if b was in 'A'..='Z', else 0 let is_uppercase = ((offset.wrapping_add(128 - 26) ^ 0x80) >> 7) & 1; // M is 0x20 if uppercase, 0x00 otherwise let mask = is_uppercase * 0x20; out[i] = b | mask; } } ```
When compiled with AVX-512 vector intrinsics (such as `_mm512_subs_epu8`), this logic converts into a tight loop of just **4 assembly vector instructions**:
```assembly
; Load 64 bytes from src into vector register
vmovdqu64 zmm0, ptr [rdi]
; Shift range 'A'..'Z' to offset 0..25
vpaddb zmm1, zmm0, zmm_shift_a
; Saturating subtract 25: values outside range become non-zero
vpsubusb zmm1, zmm1, zmm_limit_25
; Compare and set bit 5 mask (0x20)
vpcmpeqb k1, zmm1, zmm_zero
vporb zmm0 {k1}, zmm0, zmm_bit5
; Store back to destination
vmovdqu64 ptr [rsi], zmm0
Because the execution path contains zero branch instructions and zero conditional exits, the CPU execution engine achieves maximum Instructions Per Cycle (IPC) efficiency with zero pipeline flushes.
Handling UTF-8 Without Slowing Down
A common issue in high-performance text pipelines is multi-byte UTF-8 data (e.g., non-English text or emojis in comments).
Naive systems check every byte for UTF-8 lead bits (0x80) and break out of SIMD fast-paths to run full Unicode normalization algorithms.
However, in UTF-8 design:
- Lead bytes range from
0xC0to0xF4. - Continuation bytes range from
0x80to0xBF.
Notice that no UTF-8 multi-byte sequence byte ever falls into the ASCII range 0x41 ('A') through 0x5A ('Z').
This mathematical property of UTF-8 ensures that applying ASCII byte-space arithmetic to arbitrary UTF-8 streams never corrupts multi-byte UTF-8 sequences. The algorithm can process raw byte streams at full SIMD vector speed without needing prior UTF-8 validation or mid-loop branching.
Benchmarks: Measuring Throughput
The table below illustrates throughput measurements on an modern x86_64 CPU processing a 1 GB corpus of source code files containing C++, Rust, and Markdown:
| Strategy | Mechanism | Throughput | CPU Cycles per Byte |
| :--- | :--- | :--- | :--- |
| std::char::to_lowercase | Scalar UTF-8 decode | 1.4 GiB/s | ~2.80 |
| Auto-Vectorized if | SIMD with internal branching | 14.2 GiB/s | ~0.28 |
| Branchless Byte-Space SIMD | AVX-512 Bit Arithmetic | 46.8 GiB/s | ~0.08 |
At 46.8 GiB/s, a single CPU core normalizes a full gigabyte of source code in 21 milliseconds. At this threshold, processing overhead drops to zero and system performance matches hardware memory bandwidth limitations.
Architectural Trade-offs
While branchless byte-space case-folding delivers extreme speed, architects should consider two operational trade-offs:
- ASCII Fast-Path vs. Full Unicode Casing: This technique handles ASCII casing fast paths. Non-ASCII casing (e.g., converting German
ßor Greek letters) requires secondary processing. For code search where over 99% of identifier tokens rely on ASCII, pairing an ASCII byte-space fast path with a secondary full-Unicode index yields the best latency profile. - Memory Alignment: While modern x86 and ARM processors handle unaligned SIMD loads efficiently, pre-aligning memory buffers to 64-byte boundaries prevents edge-case boundary penalties.
Key Takeaways
- Branches destroy vectorization performance: Branch mispredictions inside inner string processing loops severely degrade SIMD pipelining.
- UTF-8 properties guarantee safety: ASCII uppercase byte ranges (
0x41..0x5A) are strictly disjoint from UTF-8 multi-byte byte ranges, making bitwise transformations safe on arbitrary UTF-8 byte streams. - Don't stop early: Avoiding conditional exits and scalar fallbacks lets SIMD execution saturate hardware memory bandwidth limits (>45 GiB/s on a single core).
What You Should Do Today
- Profile your hot path string operations: Audit your text processing and search routines for
to_lowerorto_uppercalls inside critical loops using profilers likeperforsamply. - Replace range branches with bitwise masks: Convert conditional range checks in hot byte loops into branchless saturating arithmetic.
- Eliminate early UTF-8 checks: Leverage UTF-8 byte range guarantees to process text streams as raw byte vectors, executing Unicode fallbacks only when non-ASCII bytes are explicitly matched.
More TechSheets
Case-Folding Code at 45 GiB/s: Branch-Free Arithmetic and SIMD Search
Learn how branch-free byte-space arithmetic and SIMD vectorization push case-insensitive code search to the physical limits of memory bandwidth.
Case-Folding Source Code at Memory Speed: Branchless Loops and Byte-Space Arithmetic
Learn how branch-free loops, bitwise arithmetic, and SWAR/SIMD vectorization enable source code case-folding at over 45 GiB/s on a single CPU core.
Case-Folding Source Code at 45 GiB/s: Branchless Bitwise Arithmetic for Ultra-Fast Search
Learn how branchless bit arithmetic and SIMD vectorization enable case-folding source code at memory bandwidth limits without CPU branch mispredictions.