{"id":4703,"date":"2026-07-31T16:56:20","date_gmt":"2026-07-31T16:56:20","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/31\/dont-stop-early-case-folding-source-code-at-memory-speed\/"},"modified":"2026-07-31T16:56:20","modified_gmt":"2026-07-31T16:56:20","slug":"dont-stop-early-case-folding-source-code-at-memory-speed","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/31\/dont-stop-early-case-folding-source-code-at-memory-speed\/","title":{"rendered":"Don\u2019t stop early: Case-folding source code at memory speed"},"content":{"rendered":"<p class=\"wp-block-paragraph\">Suppose a user searches for caf\u00e9 and your corpus contains CAF\u00c9, or they type stra\u00dfe and you\u2019ve stored STRASSE. To make these count as matches, you need a canonical form that erases case distinctions, so that two strings which differ only in case compare equal. That form is case folding, and it shows up wherever text is matched rather than displayed: search engines, regex (?i) flags, case-insensitive usernames and hostnames.<\/p>\n<p class=\"wp-block-paragraph\">It\u2019s a basic operation, but at GitHub we run it a lot. Blackbird, GitHub\u2019s code search engine, indexes over 180 million repositories\u2014more than 480TB of source code. Every byte is case-folded before we extract ngrams and build the index, and for every potential query result, another (implicit or explicit) case folding operation is needed to locate matches. At that scale, the speed of even a basic operation starts to matter.<\/p>\n<p class=\"wp-block-paragraph\">This post is about how we made it fast, and it starts somewhere counterintuitive: the biggest win in the ASCII fast path came from removing an optimization, not adding one. It turns out to be faster to sweep the whole buffer with no branches than to stop early at the first non-ASCII byte. We open-sourced the result as a Rust crate called casefold.<\/p>\n<h2 class=\"wp-block-heading\">Folding is not lowercasing<\/h2>\n<p class=\"wp-block-paragraph\">It is tempting to reach for <code>str::to_lowercase<\/code>, but lowercasing and folding are different operations with different goals:<\/p>\n<p class=\"wp-block-paragraph\">Lowercasing is for display, and it\u2019s locale- and context-sensitive: Greek final sigma lowercases to \u03c2 at the end of a word and \u03c3 elsewhere, and Turkish I lowercases differently than English I. Case folding is for comparison, and it\u2019s deliberately context-free and locale-independent. The point is a relation that stays stable and symmetric, so that if A folds to match B, B folds to match A in any locale. The Unicode Character Database ships an explicit CaseFolding.txt for exactly that.<\/p>\n<p class=\"wp-block-paragraph\">The two operations diverge on real characters\u2014\u00df, \u0130, final sigma\u2014which is why lowercasing as a stand-in silently produces wrong matches. This crate implements only the simple (1-to-1) folds\u2014statuses C and S in <code>CaseFolding.txt<\/code>\u2014and not the multi-character \u201cfull\u201d folds (\u00df \u2192 ss) or Turkic locale folds (the dotted \u0130). This isn\u2019t an unusual choice: common tools and regex engines like ripgrep make the same restriction, and being consistent across tools is important.<\/p>\n<h2 class=\"wp-block-heading\">The counterintuitive core: Don\u2019t stop early<\/h2>\n<p class=\"wp-block-paragraph\">We deal mostly with source code, so the text we fold is overwhelmingly ASCII and making it run at memory speed is the single most important thing we can do. Everything else just has to keep the rare non-ASCII path from spoiling it.<\/p>\n<p class=\"wp-block-paragraph\">The fold of an ASCII letter is trivial\u2014<code>A..=Z<\/code> map to <code>a..=z<\/code>, everything else is unchanged\u2014so the ASCII pass is really just \u201csweep the buffer, lowercase in place.\u201d Ask any LLM for it and you might get something like this:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>let bytes = s.as_bytes_mut(); \nfor (i, b) in bytes.iter_mut().enumerate() { \n    if *b &gt;= 0x80 { \n        break; \/\/ non-ASCII at index i: hand the rest to the Unicode path \n    } \n    if b.is_ascii_uppercase() { \n        *b += 32; \/\/ 'A'..='Z' \u2192 'a'..='z' \n    } \n}<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">It looks ideal: do the cheap byte work, and the instant you hit a non-ASCII byte, break and let the \u201creal\u201d Unicode path take over: \u201conly do the cheap work until you have to.\u201d On an Apple M4 this runs at about <strong>3 GiB\/s<\/strong>. That sounds fine in isolation, but it is more than <strong>15\u00d7 short<\/strong> of \u201coptimal\u201d because of the if branches.<\/p>\n<p class=\"wp-block-paragraph\">Let\u2019s delete every branch, line by line:<\/p>\n<ul class=\"wp-block-list\">\n<li><code>if b &gt;= 0x80 { break }<\/code> \u2192 don\u2019t stop at all. <code>OR<\/code>every byte into an accumulator and test it <em>once<\/em>, after the loop: <code>high_bit_acc |= *b<\/code>. Same information (was there any non-ASCII byte?), zero branches in the body.<\/li>\n<li><strong>The<\/strong> <code>A..=Z<\/code> <strong>range test<\/strong> \u2192 make it arithmetic. <code>b.wrapping_sub(b'A') &lt; 26<\/code> is true exactly for <code>A..=Z<\/code> (any other byte wraps to \u2265 26), yielding a 0\/1 mask with no branch.<\/li>\n<li><strong>The conditional write<\/strong> \u2192 fold the mask into the store.<code>| (is_upper &lt;&lt; 5)<\/code>sets bit 5\u2014turning an upper-case letter lower-case and being a no-op on everything else\u2014the byte is always written, never branched on.<\/li>\n<\/ul>\n<p class=\"wp-block-paragraph\">What\u2019s left has no branch in its body and no early exit:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>let mut high_bit_acc: u8 = 0; \nfor b in &amp;mut bytes { \n    high_bit_acc |= *b; \/\/ detect any non-ASCII byte \n    let is_upper = b.wrapping_sub(b'A') &lt; 26; \/\/ branchless A..=Z test \n    *b |= u8::from(is_upper) &lt;&lt; 5; \/\/ set bit 5 \u2192 lowercase, else no-op \n} \nif high_bit_acc &amp; 0x80 == 0 { \n    return bytes; \/\/ pure ASCII: already folded in place, no second buffer \n}<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">A loop with no data-dependent control flow is trivially vectorizable: LLVM emits 16-byte-at-a-time NEON and the whole thing runs at &gt; <strong>45 GiB\/s<\/strong>\u2014essentially memory bandwidth. And we come out of the pass already knowing, from <code>high_bit_acc<\/code>, whether there\u2019s any non-ASCII work left to do.<\/p>\n<p class=\"wp-block-paragraph\">How much did each step matter? Measuring the cumulative ladder on pure ASCII (Apple M4, 5.7 KB buffer):<\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Version<\/strong>\u00a0<\/th>\n<th><strong>Throughput<\/strong>\u00a0<\/th>\n<th><strong>Vectorized?<\/strong>\u00a0<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>naive (break + branch test)\u00a0<\/td>\n<td>3.1 GiB\/s\u00a0<\/td>\n<td>no (0 vector\u00a0instrs)\u00a0<\/td>\n<\/tr>\n<tr>\n<td>\u2192 branchless test\/write,\u00a0<em>keep<\/em>\u00a0break\u00a0<\/td>\n<td>2.6 GiB\/s\u00a0<\/td>\n<td>no (0 vector\u00a0instrs)\u00a0<\/td>\n<\/tr>\n<tr>\n<td>\u2192 drop the early-exit break\u00a0<\/td>\n<td>7.6 GiB\/s\u00a0<\/td>\n<td><strong>partially<\/strong>\u00a0(25 vector\u00a0instrs)\u00a0<\/td>\n<\/tr>\n<tr>\n<td>\u2192 branchless test + write (the loop)\u00a0<\/td>\n<td><strong>&gt;45\u00a0GiB\/s<\/strong>\u00a0<\/td>\n<td>fully (41 vector\u00a0instrs)\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\">The early-exit is what gates vectorization: keep the break but make the body perfectly branch-free and you still get <strong>zero<\/strong> vector instructions (~2.6 GiB\/s); a data-dependent loop exit is enough on its own to keep the loop scalar. Only once the break is gone can the compiler vectorize. The final step\u2014making the upper-case fold branchless\u2014then turns a <em>partially<\/em> vectorized loop (which still compiles the conditional store to a compare-blend-masked-store, ~7.6 GiB\/s) into the straight-line arithmetic that hits memory bandwidth.<\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Note: Branchless is a<\/strong> <strong>pessimization<\/strong> <strong>in scalar code.<\/strong> Look again at the table: making the body branchless while <em>keeping<\/em> the break (2.6 GiB\/s) is actually <strong>slower<\/strong> than the naive branchy loop (3.1 GiB\/s). The asm explains why. The branchy version only stores a byte when it actually changes one; its conditional <code>strb<\/code>is skipped for every lowercase letter, digit and space (the vast majority of real text), and the well-predicted branch that guards it is nearly free. The branchless version replaces that rarely taken store with an <strong>unconditional<\/strong> <code>strb<\/code><strong>every iteration<\/strong>, writing back all ~5,700 bytes instead of just the handful of upper-case ones. Extra write traffic for no benefit. Branchless-write only <em>wins<\/em> once the loop vectorizes, because then the store becomes a single 16-byte vector write regardless of content, and the per-byte cost disappears. The lesson: a branchless body is worth it <strong>only<\/strong> as the enabler for vectorization. On its own, in scalar code, it can cost you.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\">There\u2019s also a middle ground, and it\u2019s what standard libraries use. Instead of testing one byte at a time, <code>[u8]::is_ascii<\/code> scans a <strong>machine word at a time<\/strong>\u2014on a 64-bit target it tests 16 bytes per iteration by OR-ing two u64 lanes and checking all their high bits with a <code>single &amp; 0x8080_8080_8080_8080<\/code> mask. You can build the ASCII fast path on top of that: chunk-scan to find the ASCII prefix, then run the branchless (vectorizable) convert over it. That keeps the early-exit ability\u2014it still bails on the first non-ASCII block\u2014while letting both halves go fast. The catch is that it reads the data <strong>twice<\/strong> (once to scan, once to convert), landing at about <strong>23 GiB\/s<\/strong>\u2014roughly half of the single-pass branchless sweep, and ~7\u00d7 the naive break loop. A solid, general-purpose default; just not the absolute ceiling when you control the whole loop and can fold detection and conversion into one branch-free pass.<\/p>\n<p class=\"wp-block-paragraph\"><strong>Wouldn\u2019t<\/strong> <strong><em>fusing<\/em><\/strong> <strong>the two passes be faster?<\/strong> It\u2019s the obvious next thought: keep the chunked early-exit but convert each 16-byte block right after you\u2019ve confirmed it\u2019s ASCII, reading the data only <em>once<\/em>. Measured, it\u2019s <strong>~2.6\u00d7 slower<\/strong>\u20148.7 GiB\/s versus the two-pass 23. The inner block convert still vectorizes to a single 16-byte op, but now there\u2019s a data-dependent early-exit branch <em>every 16 bytes<\/em>, and that branch pins the loop to one block at a time: the compiler doesn\u2019t unroll or software-pipeline across blocks, and each iteration pays the full load\u2192test\u2192branch\u2192convert\u2192store latency with nothing to hide it behind. Split into two passes, each one is clean: the scan is a branch-light, <strong>store-free<\/strong> word scan that races through memory, and the convert is the fully-vectorized branch-free sweep at &gt;45 GiB\/s. Two fast, branch-free passes beat one branchy fused pass\u2014even though the fused version touches the data half as many times. It\u2019s the same lesson one more time: in the hot loop, the branch is the enemy.<\/p>\n<h2 class=\"wp-block-heading\">Avoiding the heap<\/h2>\n<p class=\"wp-block-paragraph\">Forty-Five GiB\/s also means doing zero unnecessary allocation. <code>simple_fold<\/code> takes the input String <em>by value<\/em>, owning the heap buffer it can mutate and return it. If the OR-accumulator\u2019s high bit was clear, the input was pure ASCII already folded in place. We hand the <strong>same allocation<\/strong> straight back, no second buffer and no copy. Otherwise, we <code>memchr<\/code>to the first non-ASCII byte and scan the tail from there, leaving the output buffer <em>unallocated<\/em> (a null write cursor) until we hit a character that folds to <strong>different bytes<\/strong>. Text whose multibyte content never folds\u2014CJK, Hangul, Kana, Arabic, Hebrew, symbols\u2014also returns the original allocation untouched, never copying a byte.<\/p>\n<p class=\"wp-block-paragraph\">Why a <em>second<\/em> buffer rather than rewriting in place like the ASCII pass? Because folding can make the string <strong>longer<\/strong>: almost every fold preserves the UTF-8 length or shrinks it, but two outliers grow\u2014<code>U+023A<\/code> (\u023a) and <code>U+023E<\/code> (\u2c7f) are 2 bytes each yet fold to 3-byte characters (\u2c65, \u0240). Once one appears, the output no longer fits in the input\u2019s bytes, and we need somewhere new to write.<\/p>\n<p class=\"wp-block-paragraph\">We allocate that buffer <strong>once<\/strong>, sized for the worst case, rather than growing it as more folds appear. Incremental reserve calls would mean re-checking capacity, occasionally reallocating, copying everything written so far, and juggling extra length\/capacity bookkeeping; a single up-front allocation lets a raw write cursor run straight to the end with none of that. And since the cursor is <code>null<\/code>until that first growing\/changing fold, it doubles as the \u201chave we allocated the extra buffer yet?\u201d flag.<\/p>\n<p class=\"wp-block-paragraph\">Sizing it needs a bound on growth, and those same two outliers give it: every 2 input bytes yield at most 3 output bytes, capping the output at <strong>1.5\u00d7 the input<\/strong>\u2014exactly the capacity we reserve:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>out = Vec::with_capacity(bytes.len() + bytes.len() \/ 2 + 4); <\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">After that the loop writes through a raw pointer with no capacity checks and calls <code>set_len<\/code> once at the end. Two more details keep it branch-light. The run of unchanged bytes between two folds is moved with a single <code>copy_nonoverlapping<\/code> rather than byte by byte. And each fold unconditionally writes all 4 bytes of a little-endian word before bumping the cursor by only the <em>folded<\/em> length (1\u20134)\u2014dropping a branch on the output length from the hot path, with the + 4 in the reservation as the headroom that makes the final character\u2019s over-store safe.<\/p>\n<h2 class=\"wp-block-heading\">Making Unicode cheap too<\/h2>\n<p class=\"wp-block-paragraph\">When a character <em>does<\/em> fold, we still don\u2019t want to fall off a cliff\u2014decode UTF-8, hash lookup, re-encode. Unicode 16.0 has 1484 simple-fold mappings, but they\u2019re a <em>very<\/em> sparse and <em>very<\/em> structured relation. Four observations shrink them to <strong>1776 bytes<\/strong> and let the fold run <strong>without ever decoding a full character<\/strong>.<\/p>\n<p class=\"wp-block-paragraph\">Even on the non-ASCII path, the overwhelming majority of characters do not fold. The hot operation isn\u2019t really \u201cfold this character,\u201d it\u2019s \u201cdoes this character fold?\u201d Almost always no. The table has to make that <strong>negative test<\/strong> as cheap as possible; the actual folding is the rare case on an already-rare path. That priority is what shapes the layout below\u2014the page bitmap exists precisely so a non-folding character is rejected in a single bit test, straight from its leading UTF-8 bytes, without decoding or scanning anything.<\/p>\n<p class=\"wp-block-paragraph\">This is exactly why a <code>HashMap&lt;u32, u32&gt;<\/code> is the <em>wrong<\/em> shape for the job, not just a bigger one. A hash map is optimized for the <strong>hit<\/strong>: it finds a present key in roughly one probe, and only spends extra work (more probes, full key comparison) when load factor or collisions bite. But our workload is dominated by <strong>misses<\/strong>\u2014characters that aren\u2019t in the table at all\u2014and a miss is a hash map\u2019s <em>least<\/em> favorite query: it still has to hash the key, jump to a bucket, and walk the probe sequence far enough to <em>prove absence<\/em>.<\/p>\n<h2 class=\"wp-block-heading\">Foldable code points cluster into 64-code-point \u201cpages\u201d<\/h2>\n<p class=\"wp-block-paragraph\">Foldable code points bunch together. Slice the code space into 64-code-point \u201cpages\u201d and the ~1484 folds touch just <strong>59<\/strong> of ~1960 possible pages. A one-bit-per-page <strong>presence<\/strong> <strong>bitmap<\/strong> answers the negative test on its own: a clear bit is a <em>definitive<\/em> \u201cno fold\u201d\u2014copy through, done\u2014which is what makes fold-free scripts cheap. Only on a set bit do we consult a second structure, a <strong>cumulative-popcount side table<\/strong> that ranks the page (how many populated pages precede it) to find its slice of entries, storing nothing for the ~1900 empty pages.<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>let (word_idx, bit_idx, c_len) = if lead &lt; 0xE0 { \n    (0usize, lead &amp; 0x1F, 2usize) \/\/ 2-byte: word 0 \n} else if lead &lt; 0xF0 { \n    ((lead &amp; 0x0F) as usize, bytes[read + 1] &amp; 0x3F, 3) \/\/ 3-byte: word = nibble \n \n} else { \n    ( \n        (((lead &amp; 0x07) as usize) &lt;&lt; 6) | (bytes[read + 1] &amp; 0x3F) as usize, \n        bytes[read + 2] &amp; 0x3F, \n        4usize, \n    ) \/\/ 4-byte: merge 2 bytes \n}; \n\/\/ reject without decoding: clear bit \u21d2 no fold \nif word_idx &gt;= PAGE_BITMAP.len() || (PAGE_BITMAP[word_idx] &gt;&gt; bit_idx) &amp; 1 == 0 { \n    read += c_len; \n    continue; \n} <\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Because <code>word_idx<\/code>depends only on the lead byte (and, for four-byte sequences, the first continuation byte), the bitmap load can be issued early.<\/p>\n<h2 class=\"wp-block-heading\">Within a page, folds come in runs<\/h2>\n<p class=\"wp-block-paragraph\">A set page bit tells us <em>something<\/em> on this page folds, but not which code points or to what. The obvious encoding is one entry per foldable code point\u2014but that is both bulky and slow to search: a page can hold dozens of folds, and we\u2019d have to scan them all to find the one matching the current code point. The structure of the data rescues us again. Adjacent code points overwhelmingly share the same delta to their fold: A\u2013Z all map <code>+32<\/code>, and Latin Extended is full of <em>alternating<\/em> runs like <code>0x0100<\/code>, <code>0x0102<\/code>, <code>0x0104<\/code>, \u2026 where every second code point folds. Instead of per-code-point entries we store <strong>runs<\/strong>\u2014start, end, stride, delta\u2014and a 1-bit stride flag covers both the contiguous and the every-other case. This interval compression collapses the ~1484 individual folds into just <strong>238<\/strong> runs across the 59 pages (\u2248four per page), leaving the within-page search only a handful of entries to look at instead of dozens. This range-with-delta encoding (including the stride trick) is borrowed from Go\u2019s unicode package, whose <a href=\"https:\/\/github.com\/golang\/go\/blob\/master\/src\/unicode\/tables.go\"><code>CaseRange<\/code><\/a> records store a Lo\/Hi range plus per-case deltas, with an <code>UpperLower<\/code> sentinel marking the alternating blocks. Runs are split at the page boundaries so a run never straddles two pages.<\/p>\n<h2 class=\"wp-block-heading\">A run record is two clean bytes<\/h2>\n<p class=\"wp-block-paragraph\">With both endpoints inside one page they fit in 6 bits, split across two arrays: <code>RUN_END_LOW[``i``] = end &amp; 0x3F<\/code> (the scan key) and <code>RUN_START_STRIDE[``i``] = (start &amp; 0x3F) | ((stride \u2212 1) &lt;&lt; 6)<\/code> (read only on a hit). Because each key is one clean byte, the within-page search can go <strong>wide<\/strong>: rather than comparing <code>cp &amp; 0x3F<\/code> against the runs one at a time, we load <strong>8 end_low bytes into a single u64 and test all of them at once<\/strong> with one branchless SWAR step\u2014<code>(chunk | 0x80\u202680) \u2212 broadcast(low) &amp; 0x80\u202680<\/code> sets the top bit of every lane whose key is <code>\u2265 cp &amp; 0x3F<\/code>. A single bit-scan of that mask (the keys are sorted, so the first set lane is the run we want) finds the slot. A page holds ~4 runs on average; that one 8-wide compare almost always resolves the entire search in a single step. One unlucky page does hold 30 runs, which puts the compare inside a short loop that strides eight keys at a time\u2014but that loop trips at most a handful of times on exactly one page in all of Unicode, and never on the common ones. Either way: no per-run branch, and no code-point reconstruction anywhere.<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>\/\/\/ Offset of the first run with `end_low &gt;= low_v` in a page of `n` runs, \n\/\/\/ or `n` if none. Scans 8 `end_low` bytes at a time via SWAR. \n#[inline] \nfn scan_end_low(lo: usize, n: usize, low_v: u8) -&gt; usize { \n    const HIGH: u64 = 0x8080_8080_8080_8080; \n    const ONES: u64 = 0x0101_0101_0101_0101; \n    let bcast = (low_v as u64).wrapping_mul(ONES); \n    let mut base = 0; \n    while base &lt; n { \n        \/\/ RUN_END_LOW is padded by 8 bytes so this read is always in bounds. \n        let chunk = u64::from_le_bytes( \n            RUN_END_LOW[lo + base..lo + base + 8] \n                .try_into() \n                .expect(\"8-byte slice\"), \n        ); \n        \/\/ `(b | 0x80) - low_v` keeps its high bit iff `b &gt;= low_v` (no \n        \/\/ cross-lane borrow). The first set lane is the first run `&gt;= low_v`. \n        let ge = (chunk | HIGH).wrapping_sub(bcast) &amp; HIGH; \n        if ge != 0 { \n            let j = base + (ge.trailing_zeros() \/ 8) as usize; \n            return if j &lt; n { j } else { n }; \n        } \n        base += 8; \n    } \n    n \n} <\/code><\/pre>\n<\/div>\n<h2 class=\"wp-block-heading\">Folding is a little-endian byte addition<\/h2>\n<p class=\"wp-block-paragraph\">On a little-endian machine the folded character\u2019s UTF-8 bytes, read as a <code>u32<\/code>, equal the source bytes (as a <code>u32<\/code>) plus a <strong>per-run constant<\/strong>. A parallel <code>BYTE_DELTA[i]<\/code> table then turns the whole fold into a masked load, one <code>wrapping_add<\/code>, and a 4-byte store:<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>let word = u32::from_le_bytes(next_four_bytes) &amp; length_mask; \/\/ keep this char's bytes \nlet folded = word.wrapping_add(BYTE_DELTA[i]); \/\/ the fold, as one byte add \nwrite_u32_le(dst, folded); \/\/ store all 4 bytes... \ndst += utf8_len(folded); \/\/ ...advance by the folded length<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Both lengths in that snippet\u2014the <code>length_mask<\/code> for the source character and the <em>advance by the folded length<\/em> for the destination\u2014come from one more tiny trick. A UTF-8 sequence\u2019s length is fixed by the top four bits of its lead byte, letting the 16 possible lengths pack one nibble each into a single 64-bit constant (<code>0x4322_1111_1111_1111<\/code>); the length is then a shift and a mask, <code>(LEN_BITS &gt;&gt; (4 * (lead &gt;&gt; 4))) &amp; 0xF<\/code>\u2014no if chain, no table memory, nothing for the predictor to get wrong. (A <em>count leading ones<\/em>\u2014<code>(!lead).leading_zeros()<\/code>\u2014would also work, since a lead byte carries one leading 1-bit per byte of the sequence.)<\/p>\n<div class=\"wp-block-code-wrapper\">\n<pre class=\"wp-block-code language-plaintext\"><code>\/\/\/ Number of bytes in the UTF-8 sequence whose lead byte is `lead`. \n#[inline] \npub fn utf8_len(lead: u8) -&gt; usize { \n    const UTF8_LEN_BY_LEAD: u64 = 0x4322_1111_1111_1111; \n    ((UTF8_LEN_BY_LEAD &gt;&gt; (4 * (lead &gt;&gt; 4))) &amp; 0xF) as usize \n}<\/code><\/pre>\n<\/div>\n<p class=\"wp-block-paragraph\">Because we advance by the <em>folded<\/em> length, this even handles length-changing folds\u2014<code>U+212A<\/code> KELVIN SIGN (3 bytes) \u2192 k (1 byte), or <code>U+023A<\/code> \u023a (2 bytes) \u2192 <code>U+2C65<\/code> \u2c65 (3 bytes)\u2014by writing fewer or more bytes than were read. That\u2019s the part we believe is genuinely new: every other folder we looked at\u2014ICU, Go\u2019s unicode, Rust\u2019s regex, CPython, glibc\u2014decodes UTF-8 to a code point, applies the fold there, and re-encodes (even SIMD folders decode first). Doing the arithmetic in byte space skips both the decode and the encode, which is exactly why this path can outrun a hash map that already has the answer tabulated\u2014the hash map still has to decode its key and encode its result. The byte-space arithmetic assumes the input is <strong>well-formed, shortest-form UTF-8<\/strong>\u2014every code point encoded with the minimal number of bytes. Reading the source bytes as a <code>u32<\/code>and adding a per-run delta only lands on the correct folded encoding when the source is in canonical form; an <em>overlong<\/em> encoding (a code point padded into more bytes than necessary, e.g. \/ as <code>0xC0 0xAF<\/code>) has a different byte pattern and would break the<code>length_mask<\/code> and the delta arithmetic. This is not a real restriction in Rust\u2014<code>&amp;str<\/code>\/<code>String<\/code> are guaranteed to hold valid UTF-8, which by definition rejects overlong sequences\u2014but a caller feeding raw bytes from elsewhere must validate (or otherwise normalize) them first.<\/p>\n<h2 class=\"wp-block-heading\">The ASCII shortcut in the tail loop<\/h2>\n<p class=\"wp-block-paragraph\">One more shortcut rounds out the tail loop. Remember the first pass already lowercased every ASCII byte, so when the scan meets an ASCII byte in the tail it advances a single byte and moves on\u2014no page probe, no table touch at all. And it doesn\u2019t copy that byte either: unmodified bytes (ASCII and non-folding multibyte alike) aren\u2019t moved one at a time. The scan just keeps walking until it reaches a character that actually folds, then flushes the whole unchanged run between the last fold and this one with a single <code>copy_nonoverlapping<\/code>. Mixed text\u2014CJK with ASCII spaces and punctuation, or code with the occasional accented identifier\u2014therefore races through the ASCII filler and only consults the bitmap for genuine multibyte characters, copying in bulk rather than byte by byte.<\/p>\n<h2 class=\"wp-block-heading\">Putting it together: the whole table<\/h2>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Component<\/strong>\u00a0<\/th>\n<th><strong>Bytes<\/strong>\u00a0<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>PAGE_BITMAP (1 bit per 64-cp page)\u00a0<\/td>\n<td>248\u00a0<\/td>\n<\/tr>\n<tr>\n<td>POPCNT_SAMPLES (cumulative\u00a0popcount)\u00a0<\/td>\n<td>32\u00a0<\/td>\n<\/tr>\n<tr>\n<td>PAGE_OFFSET (per populated page)\u00a0<\/td>\n<td>60\u00a0<\/td>\n<\/tr>\n<tr>\n<td>RUN_END_LOW (scan key, end &amp; 0x3F, +8 pad)\u00a0<\/td>\n<td>246\u00a0<\/td>\n<\/tr>\n<tr>\n<td>RUN_START_STRIDE (start &amp; 0x3F | stride)\u00a0<\/td>\n<td>238\u00a0<\/td>\n<\/tr>\n<tr>\n<td>BYTE_DELTA (little-endian fold delta per run)\u00a0<\/td>\n<td>952\u00a0<\/td>\n<\/tr>\n<tr>\n<td><strong>Total<\/strong>\u00a0<\/td>\n<td><strong>1776<\/strong>\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\">That\u2019s <strong>9.6 bits per fold entry<\/strong>, over half of it the <code>BYTE_DELTA<\/code> side table we trade for the decode-free path; the index + run records alone are ~4.4 bits\/entry.<\/p>\n<p class=\"wp-block-paragraph\">Next to the obvious alternatives, that 1776 bytes is an order of magnitude or more smaller\u2014and unlike most of them, it never decodes a character:<\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Representation<\/strong>\u00a0<\/th>\n<th><strong>Size<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Na\u00efve\u00a0[(u32, u32); 1484]\u00a0<\/td>\n<td>~11.6 KB\u00a0<\/td>\n<\/tr>\n<tr>\n<td>regex-syntax\u2019s\u00a0case_folding_simple\u00a0table\u00a0<\/td>\n<td>~70 KB\u00a0<\/td>\n<\/tr>\n<tr>\n<td>Go\u2019s\u00a0unicode.SimpleFold\u00a0(orbit + ASCII + ranges)\u00a0<\/td>\n<td>~7.3 KB\u00a0<\/td>\n<\/tr>\n<tr>\n<td>A runtime\u00a0HashMap&lt;u32, u32&gt;\u00a0<\/td>\n<td>~17 KB\u00a0<\/td>\n<\/tr>\n<tr>\n<td><strong>This crate (paged bitmap + packed runs)<\/strong>\u00a0<\/td>\n<td><strong>1776 B<\/strong>\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2 class=\"wp-block-heading\">Where it lands against the alternatives<\/h2>\n<p class=\"wp-block-paragraph\">On the common case, ASCII, folding runs at memory bandwidth (&gt;45 GiB\/s), more than an order of magnitude ahead of other real folders and more than 50% faster than the (non-equivalent) <code>str::to_lowercase<\/code> function. To get a rough \u201cupper bound\u201d for the non-ASCII case, we measured the optimized Utf8 decoding + encoding round trip without performing any actual case folding using the simdutf crate. This experiment achieves consistently about 2GB\/sec and is only about twice as fast than our solution for the worst case all-folding input. A naive hash map trails everything on all workloads.<\/p>\n<p class=\"wp-block-paragraph\">The three columns are real case folders that produce identical output: simple_fold (this crate), simd_normalizer (the simd-normalizer crate), and HashMap (naive CaseFolding.txt lookup). The workload rows are chosen to simulate different scenarios from typical to worst case:<\/p>\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<thead>\n<tr>\n<th><strong>Workload (input size)<\/strong>\u00a0<\/th>\n<th><strong>simple_fold<\/strong>\u00a0<\/th>\n<th><strong>simd_normalizer<\/strong>\u00a0<\/th>\n<th><strong>HashMap (byte path)<\/strong>\u00a0<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Pure ASCII (5.7 KB)\u00a0<\/td>\n<td><strong>&gt;45\u00a0GiB\/s<\/strong>\u00a0<\/td>\n<td>1.21 GiB\/s\u00a0<\/td>\n<td>213 MiB\/s\u00a0<\/td>\n<\/tr>\n<tr>\n<td><strong>C<\/strong>hinese\/<strong>J<\/strong>apanese\/<strong>K<\/strong>orean, no folds (8.1 KB)\u00a0<\/td>\n<td><strong>2.95 GiB\/s<\/strong>\u00a0<\/td>\n<td>1.97 GiB\/s\u00a0<\/td>\n<td>558 MiB\/s\u00a0<\/td>\n<\/tr>\n<tr>\n<td>Symbols \/ Myanmar, no folds (9.0 KB)\u00a0<\/td>\n<td><strong>2.96 GiB\/s<\/strong>\u00a0<\/td>\n<td>1.56 GiB\/s\u00a0<\/td>\n<td>410 MiB\/s\u00a0<\/td>\n<\/tr>\n<tr>\n<td>Worst case: Latin\/Greek\/Cyrillic (Unicode U+0000\u2013U+FFFF), all folding (8.8 KB)\u00a0<\/td>\n<td>869 MiB\/s\u00a0<\/td>\n<td><strong>922 MiB\/s<\/strong>\u00a0<\/td>\n<td>334 MiB\/s\u00a0<\/td>\n<\/tr>\n<tr>\n<td>Length-changing folds (1.7 KB)\u00a0<\/td>\n<td><strong>1.26 GiB\/s<\/strong>\u00a0<\/td>\n<td>716 MiB\/s\u00a0<\/td>\n<td>233 MiB\/s\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p class=\"wp-block-paragraph\"><em>Treat the absolute figures as illustrative, not portable: the whole design leans on auto-vectorization, SWAR, and little-endian byte arithmetic, so the numbers\u2014and even the ratios between rows\u2014can shift substantially on a different microarchitecture (a wider or narrower vector unit, different memory bandwidth, a big-endian target, x86 vs ARM).<\/em><\/p>\n<p class=\"wp-block-paragraph\">More details can be found in the <a href=\"https:\/\/github.com\/github\/rust-gems\/blob\/main\/crates\/casefold\/README.md#performance\">performance section of the README<\/a>.<\/p>\n<h2 class=\"wp-block-heading\">Take this with you<\/h2>\n<p class=\"wp-block-paragraph\">Case folding is about as basic as text operations get, which is exactly why it was worth the effort: we run it across every byte we index. The wins came from two ideas that both cut against instinct\u2014sweep the whole buffer branch-free instead of stopping early, and do the fold as byte-space arithmetic instead of decoding to a code point. Together they let the common case run at memory bandwidth and the rare fold run without a decode, in a table small enough (1776 bytes) to stay resident. The decode-free byte-space fold is the piece we believe is genuinely new; it\u2019s why this path can beat a hash map that already has the answer.<\/p>\n<p class=\"wp-block-paragraph\">There\u2019s surely more to find here, and we\u2019d like to see it. The crate is <a href=\"https:\/\/crates.io\/crates\/casefold\">casefold<\/a>; the generated table and full design notes live alongside the source.<\/p>\n<p>The post <a href=\"https:\/\/github.blog\/engineering\/architecture-optimization\/dont-stop-early-case-folding-source-code-at-memory-speed\/\">Don\u2019t stop early: Case-folding source code at memory speed<\/a> appeared first on <a href=\"https:\/\/github.blog\/\">The GitHub Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Suppose a user searches for caf\u00e9 and your corpus contains CAF\u00c9, or they type stra\u00dfe and you\u2019ve stored STRASSE. To [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[8],"tags":[],"class_list":["post-4703","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-github-engineering"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4703","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4703"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4703\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}