{"id":50443873,"url":"https://github.com/ericrihm/guid-race","last_synced_at":"2026-05-31T20:02:46.870Z","repository":{"id":356155763,"uuid":"1231248091","full_name":"ericrihm/guid-race","owner":"ericrihm","description":"1 billion GUID-to-string conversions per second. Response to Dave Plummer's challenge from Dave's Garage.","archived":false,"fork":false,"pushed_at":"2026-05-06T21:40:15.000Z","size":27,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T22:26:41.698Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericrihm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T19:25:16.000Z","updated_at":"2026-05-06T21:40:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ericrihm/guid-race","commit_stats":null,"previous_names":["ericrihm/guid-race"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ericrihm/guid-race","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fguid-race","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fguid-race/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fguid-race/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fguid-race/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericrihm","download_url":"https://codeload.github.com/ericrihm/guid-race/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericrihm%2Fguid-race/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33746528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T20:02:46.810Z","updated_at":"2026-05-31T20:02:46.864Z","avatar_url":"https://github.com/ericrihm.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# guid-race: 1 Billion GUID-to-String Conversions Per Second\n\nA response to [Dave Plummer's challenge](https://www.youtube.com/watch?v=VYTF4KIF2z0) from *Dave's Garage*:\n\n\u003e \"If you can think of a faster way to do it, let me know in the comments.\"\n\n**Here's one. Using ARM NEON vector instructions, it's 5x faster than the nibble walker on Apple M4, and ~180x faster than `sprintf`.**\n\nThe secret: ARM NEON's `vqtbl3q` instruction builds the entire output string -- hex characters, hyphens, and all -- in a single 3-register table lookup. Hyphens aren't checked or inserted; they're *free*, just another index in the scatter table.\n\n## Results\n\n### Apple M4 (ARM64 NEON)\n\n| Implementation | Median (ns) | vs Dave | vs sprintf | ops/sec |\n|---|---:|---:|---:|---:|\n| **neon_scatter** | **1.03** | **5.5x** | **181x** | **~970M** |\n| neon_fused | 1.04 | 5.5x | 179x | ~960M |\n| neon_ultimate | 1.13 | 5.0x | 166x | ~885M |\n| neon_arith | 1.50 | 3.7x | 125x | ~667M |\n| neon_simd | 1.89 | 2.9x | 98x | ~530M |\n| neon_tbl2 | 1.89 | 2.9x | 98x | ~530M |\n| lookup16 | 3.40 | 1.6x | 55x | ~294M |\n| **dave_original** | **5.55** | **1.0x** | **33x** | **~180M** |\n| unrolled | 6.48 | 0.86x | 29x | ~154M |\n| arithmetic | 6.35 | 0.88x | 29x | ~157M |\n| swar | 7.07 | 0.79x | 27x | ~141M |\n| sprintf | 188 | 0.03x | 1.0x | ~5.3M |\n\n\u003e Methodology: 10M iterations x 11 samples, median reported. 256 random GUIDs cycled through (warm L1). Apple M4, clang -O3 -march=native.\n\u003e\n\u003e Output uses lowercase hex. Windows `StringFromGUID2` uses uppercase with braces; Dave's original likely did too.\n\n## How It Works\n\n### Dave's Original (our baseline)\n\nFrom the video, Dave's optimized version replaces `sprintf` with a nibble walker:\n\n```c\nstatic const unsigned char order[16] = {\n    3,2,1,0,  5,4,  7,6,  8,9,10,11,12,13,14,15\n};\nfor (i = 0; i \u003c 16; i++) {\n    unsigned char b = p[order[i]];\n    out[j++] = hex[b \u003e\u003e 4];\n    out[j++] = hex[b \u0026 0xF];\n    if (j == 8 || j == 13 || j == 18 || j == 23)\n        out[j++] = '-';\n}\n```\n\nThe `order` array handles the GUID's split personality -- three little-endian integers followed by raw bytes:\n\n```\nMemory layout:  04 03 02 01 | 06 05 | 08 07 | 09 0A  0B 0C 0D 0E 0F 10\n                └─ Data1 LE ┘ └ D2 ┘  └ D3 ┘  └──── Data4 (as-is) ────┘\n                    ↕ swap      ↕       ↕\nCanonical form: 01020304     - 0506  - 0708  - 090A - 0B0C0D0E0F10\n```\n\nThis is clean, correct, and ~33x faster than `sprintf`. But it has three costs:\n1. **Two table lookups per byte** (32 total)\n2. **A branch for hyphen insertion** (checked 16 times, taken 4 times)\n3. **Loop overhead** (16 iterations with serial dependency on `j`)\n\n### The Winning Approach: NEON Scatter (`neon_scatter`)\n\nProcess all 16 bytes in parallel using ARM NEON vector instructions:\n\n```\nGUID bytes ──\u003e [reorder] ──\u003e [split nibbles] ──\u003e [hex lookup] ──\u003e [zip] ──\u003e [scatter+hyphens] ──\u003e output\n   16 B          tbl(1)       shr+and(2)          tbl(2)         zip(2)      tbl3+store(4)\n```\n\nThe key insight is `vqtbl3q_u8` -- a 3-register table lookup that acts as a programmable byte scatter. We build a table of `{hex_chars_lo, hex_chars_hi, all_hyphens}` and use pre-computed index vectors to place everything in one shot:\n\n```c\n// scatter1 picks hex chars from registers 0-1 and hyphens from register 2\nstatic const uint8_t scatter1[16] = {\n     0,  1,  2,  3,  4,  5,  6,  7,   // 8 hex chars from Data1\n    32,  8,  9, 10, 11, 32, 12, 13    // hyphens at 8,13; hex chars between\n};\n//  ^-- index 32 = hyphen register     ^-- another hyphen\n\nuint8x16x3_t tbl = { zipped.val[0], zipped.val[1], vdupq_n_u8('-') };\nvst1q_u8(out,      vqtbl3q_u8(tbl, scatter1));  // output[0..15]\nvst1q_u8(out + 16, vqtbl3q_u8(tbl, scatter2));  // output[16..31]\n```\n\n**Hyphens emerge naturally from the scatter topology** -- exactly the \"elegant branchless trick\" Dave was hoping existed.\n\n```\nOutput: 01020304-0506-0708-090a-0b0c0d0e0f10\nSource: 00000000 0000 0000 1111 111111111111\n                ^    ^    ^    ^\n                └─────── 2 ───┘\n\n0 = register 0 (first 16 hex chars)    2 = register 2 (hyphens)\n1 = register 1 (last 16 hex chars)\n```\n\nThe trick relies on a non-obvious guarantee: the `q` in `vqtbl3q` means indices \u003e= 48 (outside the 3-register table) return **zero**, not garbage. Every output byte is either a valid hex character or a valid hyphen. There's no error path because there's no error.\n\n### The Assembly (17 Data-Path Instructions)\n\nClang -O3 compiles the core data path of `neon_scatter` to 17 ARM NEON instructions (plus address generation and tail handling):\n\n```asm\nldr    q0, [x0]              ; load 16-byte GUID\nldr    q1, [byte_order]      ; load endian-swap table\ntbl    v0, {v0}, v1          ; reorder bytes\nushr   v1, v0, #4            ; high nibbles\nmovi   v2, #15\nand    v0, v0, v2            ; low nibbles\nldr    q2, [hex_lut]         ; '0'..'f' lookup table\ntbl    v1, {v2}, v1          ; high nibbles -\u003e hex chars\ntbl    v0, {v2}, v0          ; low nibbles -\u003e hex chars\nzip1   v2, v1, v0            ; interleave first 16\nzip2   v3, v1, v0            ; interleave last 16\nmovi   v4, #45               ; '-' in all lanes\nldr    q0, [scatter1]        ; output layout table\ntbl    v0, {v2,v3,v4}, v0    ; build chunk 1 with hyphens\nldr    q1, [scatter2]        ; output layout table\ntbl    v1, {v2,v3,v4}, v1    ; build chunk 2 with hyphens\nstp    q0, q1, [x1]          ; store both chunks (32 bytes!)\n; + 3 instructions for tail (4 hex chars + null terminator)\n```\n\nThe compiler recognizes two adjacent 16-byte stores (`out` and `out+16`) and fuses them into a single `stp` (store pair) -- one micro-op instead of two, writing all 32 bytes in a single cycle.\n\n## All Implementations\n\n| # | Name | Technique | Platform |\n|---|---|---|---|\n| 1 | `sprintf` | Standard library `snprintf` with format string | All |\n| 2 | `dave_original` | Dave's nibble walker from the video | All |\n| 3 | `lookup16` | 256-entry uint16 table (1 lookup/byte vs 2) | All |\n| 4 | `unrolled` | Fully unrolled, zero branches, direct stores | All |\n| 5 | `arithmetic` | Branchless `nibble + '0' + 39*(nibble \u003e= 10)` | All |\n| 6 | `swar` | 32-bit packed stores, arithmetic hex conversion | All |\n| 7 | `neon_simd` | Basic NEON vectorized with temp buffer | ARM64 |\n| 8 | `neon_tbl2` | NEON with direct lane stores, no temp buffer | ARM64 |\n| 9 | `neon_scatter` | **NEON vqtbl3q scatter (winner)** | ARM64 |\n| 10 | `neon_arith` | NEON with arithmetic hex (no LUT) | ARM64 |\n| 11 | `neon_ultimate` | vqtbl4q 4-register, overlapping stores | ARM64 |\n| 12 | `neon_fused` | Byte reorder fused into scatter tables | ARM64 |\n| 13 | `ssse3_scatter` | x86 pshufb scatter (same principle as NEON) | x86_64 |\n| 14 | `sse2_basic` | SSE2 arithmetic hex, no pshufb required | x86_64 |\n\n## Surprising Findings\n\n1. **Scalar unrolled is *slower* than Dave's loop.** The M4's branch predictor handles Dave's `if` perfectly -- the pattern is fixed and short. Unrolling adds code size without reducing work.\n\n2. **`vqtbl3q` beats `vqtbl4q` -- the micro-op cliff explains it.** On Apple Firestorm ([measured by Dougall Johnson](https://dougallj.github.io/applecpu/firestorm/)), `tbl` with 1-2 source registers is 1 uop / 2-cycle latency. At 3 registers: 2 uops / 4 cycles. At 4 registers: 3 uops / 4 cycles -- same latency but 50% worse throughput. `neon_ultimate` saves 2 `zip` instructions (2 uops) by using `tbl4`, but each of its two `tbl4` calls costs 1 extra uop vs `tbl3`. Net loss: 2 uops. The `zip` + `tbl3` path wins because `zip` is cheap (1 uop each) and `tbl3` has better throughput than `tbl4`.\n\n3. **The hex LUT beats arithmetic.** Despite `neon_arith` avoiding a memory load, the `tbl` instruction used as a 16-entry lookup table is faster than the `vcgt` + `vand` + `vadd` arithmetic chain.\n\n4. **`lookup16` is the best scalar approach.** A 256-entry uint16 table (512 bytes, fits in L1) halves the lookup count and enables 16-bit stores. This is the approach to use if you can't use SIMD.\n\n5. **We're near the floor.** The transform maps 128 input bits to 37 output bytes. Each input bit influences exactly one output nibble -- no fan-out, no carry propagation, embarrassingly parallel at the bit level. The irreducible work: 1 load, 2 nibble splits, 2 hex maps, 2 interleaves, 2 scatters, 2 stores = 11 ops. `neon_fused` compiles to 16 data-path instructions (1.45x the floor). The gap is GUID endian reorder, constant materialization, and the tail store.\n\n## The Shuffle Lineage\n\nIntel's `pshufb` (SSSE3, 2006) was the first byte-granularity permutation on x86 -- a single instruction that could rearrange any of 16 bytes. The entire hex-encoding-via-SIMD technique traces back to [Wojciech Mula's nibble lookup](http://0x80.pl/notesen/2022-01-18-conv-to-hex.html) using `pshufb` as a 16-entry table.\n\nARM's `tbl`/`tbx` instructions (ARMv8, 2013) generalize the concept: variable-width source tables (1-4 registers = 16-64 bytes), and crucially, **defined behavior on out-of-range indices** -- `tbl` zeros them, `tbx` preserves the destination. This is what makes our scatter trick possible: the same instruction that does hex lookup also places hyphens, because any index pointing at register 2 (the hyphen register) is in-range. `pshufb` has similar zeroing behavior (via bit 7), but only over a single 16-byte register -- not enough for a 3-register scatter.\n\n## Limitations\n\n- Benchmarks measure warm-cache throughput (256 GUIDs cycle through L1). Real-world latency with cold caches will be higher.\n- NEON implementations require ARMv8-A. The x86 `ssse3_scatter` uses the same principle but requires SSSE3 (Core 2 or later).\n- The `neon_scatter` and `neon_fused` results are within measurement noise of each other (~0.01ns). Treat them as tied.\n\n## Building\n\n### macOS / Linux (ARM64)\n```bash\nmake          # uses Makefile\n./guid_race\n```\n\n### CMake (cross-platform)\n```bash\ncmake -B build -DCMAKE_BUILD_TYPE=Release\ncmake --build build\n./build/guid_race\n```\n\n### Windows (MSVC)\n```cmd\ncmake -B build -G \"Visual Studio 17 2022\"\ncmake --build build --config Release\nbuild\\Release\\guid_race.exe\n```\n\n## Context\n\nIn the video, Dave tells the story of optimizing `IIDtoString` in the Windows COM runtime during his early days on the OLE team at Microsoft. The original used `sprintf` with a format string -- clean and correct, but dragging a full formatting interpreter into a hot path that ran millions of times during COM initialization, marshalling, and registry lookups.\n\nDave replaced it with a nibble walker: read each byte, index into a hex table twice (high nibble, low nibble), write the characters, check for hyphen positions. About 100x faster than `sprintf`, reviewed with a \"cool, nice one,\" and absorbed into the machine.\n\n30 years later, we have SIMD. The same insight Dave had -- \"this is a fixed encoding problem, not a formatting problem\" -- extends one step further: it's a *parallel* fixed encoding problem. Every byte is independent. Every nibble maps the same way. The output layout is constant. This is exactly what vector shuffle instructions were designed for.\n\nDave's code was written for 32-bit x86 in the early 1990s, before SIMD existed on consumer hardware. Comparing it to ARM NEON on Apple Silicon in 2026 isn't an apples-to-apples contest -- it's a demonstration of how far hardware has come. The nibble walker remains an excellent scalar solution.\n\nThe most frequently serialized IID in COM history is probably IUnknown itself: `00000000-0000-0000-C000-000000000046`.\n\n## Prior Art\n\n- [crashoz/uuid_v4](https://github.com/crashoz/uuid_v4) -- SSE4.1/AVX2 UUID library\n- [zbjornson/fast-hex](https://github.com/zbjornson/fast-hex) -- AVX2 hex encoding\n- [Daniel Lemire's hex encoding analysis](https://lemire.me/blog/2022/12/23/fast-base16-encoding/)\n- [Wojciech Mula: SIMD hex encoding](http://0x80.pl/notesen/2022-01-18-conv-to-hex.html) -- the foundational `pshufb`-as-LUT technique\n- [johnnylee-sde: Fast unsigned integer to hex string](https://johnnylee-sde.github.io/Fast-unsigned-integer-to-hex-string/)\n- [Dougall Johnson: Apple Silicon CPU features](https://dougallj.github.io/applecpu/firestorm/) -- M1 Firestorm uop measurements\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrihm%2Fguid-race","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericrihm%2Fguid-race","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericrihm%2Fguid-race/lists"}