From 74d8d58465e042f2bbd364ee37dc98b78039ccb7 Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Mon, 27 Jul 2026 01:31:50 +0900 Subject: [PATCH 1/4] further optimization based on the deep analysis of V8 opt chain --- .claude/skills/benchmark/SKILL.md | 3 +- README.md | 14 +- scripts/unicode.js | 49 +++--- src/_grapheme_data.js | 19 ++- src/core.js | 39 +++-- src/grapheme.js | 266 +++++++++++++++--------------- 6 files changed, 203 insertions(+), 187 deletions(-) diff --git a/.claude/skills/benchmark/SKILL.md b/.claude/skills/benchmark/SKILL.md index 9b90c49..d5c5970 100644 --- a/.claude/skills/benchmark/SKILL.md +++ b/.claude/skills/benchmark/SKILL.md @@ -42,7 +42,8 @@ Never move test inputs by copy-paste: they contain invisible codepoints (ZWJ, va ## Size-sensitive coding notes (learned the hard way) -- esbuild inlines `const`-bound numeric literals into every use site but preserves `let` bindings. Shared numeric bounds in `src/grapheme.js` (`BMP_MAX`, `T1_MIN`, `T2_MIN`) are deliberately `let` — keep them that way. +- esbuild inlines `const`-bound numeric literals into every use site but preserves `let` bindings. That makes a shared `let` bound tempting for size, but it compiles to a mutable context slot the engine cannot fold into the comparison: hoisting the hottest bound in `src/grapheme.js` into a `let` measured up to 25% slower on the count loop, and removing the three shared bounds was *also* smaller after gzip. Spell hot-path bounds out as literals. +- Hot-loop helpers live near engine inlining cliffs — check them with `--trace-turbo-inlining` (prints `bytecode size: N`) after any edit to `cat()` or `nextState()`. TurboFan refuses callees at 460 bytecodes (`cat` sat at 453 before it was split into `cat`/`catRare`; falling off cost 15-45%), Maglev refuses at 100 (`nextState` must stay ≤ 99, worth 8-26% there). Measure the mid/low tiers with `node --no-turbofan` and `node --jitless`; a change can win big on TurboFan and still lose on Maglev. - Generator syntax is the deliberate choice over a class-based iterator: a class was 13–34% faster on V8 but cost ~+1.6 KiB Hermes bytecode after Babel class lowering (Hermes can't parse class syntax natively). Don't convert to a class without re-measuring V8, JSC, Hermes, and all three size metrics. - `codePointAt()` beat manual `charCodeAt` surrogate pairing on every measured engine (17–20% faster on Hermes) and is smaller. Don't "optimize" it back. diff --git a/README.md b/README.md index 823d5f1..44cdcee 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Utilities for text segmentation by extended grapheme cluster rules. You can split a string into graphemes by simply consuming the `splitGraphemes()` generator. -`splitGraphemes()` is a simple wrapper around `graphemeSegments()`. +It yields substrings directly, so it allocates less than `graphemeSegments()`. ```js import { splitGraphemes } from 'unicode-segmenter/grapheme'; @@ -69,7 +69,7 @@ import { splitGraphemes } from 'unicode-segmenter/grapheme'; If you need the result as an array, use `collectGraphemes()` for convenience. -This is a fast version of `[...splitGraphemes(str)]`, 2-4x faster. +This is a fast version of `[...splitGraphemes(str)]`, 1.5-2.5x faster. ```js import { collectGraphemes } from 'unicode-segmenter/grapheme'; @@ -237,8 +237,8 @@ Since [Hermes doesn't support the `Intl.Segmenter` API](https://github.com/faceb | Name | Unicode® | ESM? | Size | Size (min) | Size (min+gzip) | Size (min+br) | Size (min+zstd) | |--------------------------------------|----------|------|--------:|-----------:|----------------:|--------------:|----------------:| -| `unicode-segmenter/grapheme` | 17.0.0 | ✔️ | 8,684 | 5,133 | 2,453 | 2,220 | 2,505 | -| `unicode-segmenter/grapheme` (full*) | 17.0.0 | ✔️ | 10,494 | 5,777 | 2,732 | 2,435 | 2,796 | +| `unicode-segmenter/grapheme` | 17.0.0 | ✔️ | 7,851 | 4,911 | 2,351 | 2,113 | 2,401 | +| `unicode-segmenter/grapheme` (full*) | 17.0.0 | ✔️ | 10,220 | 5,752 | 2,646 | 2,367 | 2,705 | | `graphemer` | 15.0.0 | ✖️ | 410,435 | 95,104 | 15,752 | 10,660 | 15,911 | | `grapheme-splitter` | 10.0.0 | ✖️ | 122,254 | 23,682 | 7,852 | 4,802 | 6,753 | | `@formatjs/intl-segmenter`* | 17.0.0 | ✖️ | 268,301 | 176,759 | 45,988 | 31,701 | 45,370 | @@ -255,8 +255,8 @@ Since [Hermes doesn't support the `Intl.Segmenter` API](https://github.com/faceb | Name | Bytecode size | Bytecode size (gzip)* | |-------------------------------------|--------------:|----------------------:| -| `unicode-segmenter/grapheme` | 20,015 | 11,075 | -| `unicode-segmenter/grapheme` (full) | 20,224 | 11,213 | +| `unicode-segmenter/grapheme` | 15,897 | 9,039 | +| `unicode-segmenter/grapheme` (full) | 16,102 | 9,186 | | `graphemer` | 134,085 | 31,770 | | `grapheme-splitter` | 63,942 | 19,165 | | `@formatjs/intl-segmenter` | 329,547 | 136,751 | @@ -270,7 +270,7 @@ Retained memory per library after segmenting the benchmark corpus, measured by ` | Name | JS heap | ArrayBuffers* | |------------------------------|--------:|--------------:| -| `unicode-segmenter/grapheme` | 228 kB | 20.6 kB | +| `unicode-segmenter/grapheme` | 217 kB | 19.1 kB | | `graphemer` | 2.32 MB | 0 B | | `grapheme-splitter` | 570 kB | 0 B | | `@formatjs/intl-segmenter` | 1.95 MB | 0 B | diff --git a/scripts/unicode.js b/scripts/unicode.js index 2b9eedb..00af2b0 100755 --- a/scripts/unicode.js +++ b/scripts/unicode.js @@ -523,14 +523,18 @@ export const ${name}_cats = '${breakTable.map(row => inversed[row[2]].toString(3 /** * UAX #29 pair-wise rules for extended grapheme cluster boundaries, - * evaluated into a 16x16 decision table indexed by - * `catBefore << 4 | catAfter`. + * evaluated into a 16x16 mask table indexed by `catBefore << 4 | catAfter`. * - * - 0: boundary (GB999 and friends) - * - 1: no boundary + * Each entry is a bit mask over the runtime's packed sequence state; + * a boundary exists iff the mask and the state share no bit, so a single `!(st & PAIR[...])` resolves every rule. + * + * - 0: boundary (GB999 and friends), no state bit can match + * - 1: no boundary; bit 0 is always set in the state * - 2: GB12/GB13, no boundary iff odd run of RI precedes - * - 3: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* - * - 4: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes + * - 4: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* + * - 8: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes + * + * Every value stays a single decimal digit so that the runtime decodes the table with a bare `Uint8Array.from(grapheme_pairs)`. * * Category numbers follow the emitted `GraphemeCategory` enum: * 0 Any, 1 CR, 2 Control, 3 Extend, 4 Extended_Pictographic, 5 L, 6 LF, @@ -554,9 +558,9 @@ let buildGraphemePairTable = () => { else if (B === 5) v = (A === 5 || A === 7 || A === 8 || A === 13) ? 1 : 0; // GB6: L × (L | V | LV | LVT) else if (B === 7 || B === 13) v = (A === 13 || A === 12) ? 1 : 0; // GB7: (LV | V) × (V | T) else if (B === 8 || B === 12) v = A === 12 ? 1 : 0; // GB8: (LVT | T) × T - else if (B === 14 && A === 4) v = 3; // GB11: ExtPic Extend* ZWJ × ExtPic + else if (B === 14 && A === 4) v = 4; // GB11: ExtPic Extend* ZWJ × ExtPic else if (B === 10 && A === 10) v = 2; // GB12, GB13: RI × RI - if (v === 0 && (B === 3 || B === 14) && a === 15) v = 4; // GB9c: InCB=C [E L]* L [E L]* × InCB=C + if (v === 0 && (B === 3 || B === 14) && a === 15) v = 8; // GB9c: InCB=C [E L]* L [E L]* × InCB=C out += v; } } @@ -860,8 +864,9 @@ for (let chars of graphemeTable) { // Filter out ranges handled by inlined fast paths in cat() let graphemeTableOptimized = graphemeTable.filter(([from, to, cat]) => { - // CJK fast path: 0x3000-0x9FFF (inlined in cat()) - if (from >= 0x3000 && to <= 0x9FFF) { + // CJK fast path: 0x30A0-0xA65F (inlined in cat(); the T0 table covers + // everything below 0x30A0, the T1 table everything from 0xA660) + if (from >= 0x30A0 && to <= 0xA65F) { return false; } // Hangul syllables (LV/LVT): computed at runtime @@ -899,13 +904,8 @@ let graphemeTableOptimized = graphemeTable.filter(([from, to, cat]) => { }; /** @type {(cp: number) => string} */ let inlined = cp => { - // CJK: 0x3000-0x9FFF - if (cp >= 0x3000 && cp < 0xA000) { - if (cp < 0x3030) return cp >= 0x302A ? 'Extend' : 'Any'; - if (cp < 0x309B) { - if (cp === 0x3030 || cp === 0x303D) return 'Extended_Pictographic'; - return cp >= 0x3099 ? 'Extend' : 'Any'; - } + // CJK, Yi, Lisu, Vai: 0x30A0-0xA65F + if (cp >= 0x30A0 && cp < 0xA660) { return (cp === 0x3297 || cp === 0x3299) ? 'Extended_Pictographic' : 'Any'; } // Hangul syllables @@ -929,7 +929,7 @@ let graphemeTableOptimized = graphemeTable.filter(([from, to, cat]) => { throw new Error(`no inlined fast path covers ${cp.toString(16)}`); }; for (let [lo, hi] of [ - [0x3000, 0x9FFF], + [0x30A0, 0xA65F], [0xAC00, 0xFE0F], [0xE0000, 0xE1000], ]) { @@ -965,14 +965,15 @@ await emitSrc( f.write(` /** * UAX #29 pair-wise rules for extended grapheme cluster boundaries, - * evaluated into a 16x16 decision table indexed by - * \`catBefore << 4 | catAfter\`. + * evaluated into a 16x16 mask table indexed by \`catBefore << 4 | catAfter\`. + * + * A boundary exists iff the mask shares no bit with the runtime's packed sequence state. * - * - 0: boundary (GB999 and friends) - * - 1: no boundary + * - 0: boundary (GB999 and friends), no state bit can match + * - 1: no boundary; bit 0 is always set in the state * - 2: GB12/GB13, no boundary iff odd run of RI precedes - * - 3: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* - * - 4: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes + * - 4: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* + * - 8: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes * * See \`buildGraphemePairTable\` in scripts/unicode.js for the rules. */ diff --git a/src/_grapheme_data.js b/src/_grapheme_data.js index e8bf579..086c14d 100644 --- a/src/_grapheme_data.js +++ b/src/_grapheme_data.js @@ -98,25 +98,26 @@ export const GraphemeCategory = { * @type {UnicodeDataEncoding} */ export const grapheme_data = /** @type {UnicodeDataEncoding} */ ( - '090001000h1v5g2903000hl2v6jh16ng1s210111110o35aa10u2k1g10l6600152113h2010u1q1r5aq3890o13181214r22k3158q2n1000v100h1k20000001207030001160721k171001h1j11610332010010321210090411021c1c02100o301201412230u1130b100h1j116111420120411001100k11l10051001h1j11611142011000321210072411021d0g10r3000000132120090o200200g1j11f2012031213711271t1001o301001000001121371b1f0c101h1l2011001031212000080a1t1001m40400102101600i11t301006c7i601008b6p41r10101041h3d0004115a1j290p3q223000511010100g1501010302003172030c0010016000e0i6v50n40n5tl12ir33s12t11u11cj30100060700010a90t220000l71i20m7203010241000502rd101004k300000006101027050920g3t22bk1300616g209030207u18c1000t10003010501b2o20000102000004g37070101o921c000640602001mcv3rg20000001o16d0c0m1fg6g2h30m10q55f1vm11c0ma0p1a42nc0ne1a090q3314902021204020112030318250105bb1201110i1021i15101131502141b15120511011k1155132040202510201010603060a1f02040104210b1g3290e0e0kn11vs12j11j3040pp12t80g6v1vmo7319g21g51gh103040n11010040j51i3f01q1h1d0m27p1a0000cs13200523j200010301010100v140018a4q2501010101c08000i1f12601011g3012215110u1a0001015000pcq181000100011000iji5fvc0u91g5bhg20ie0l94lo6002115303121s12240la1tj23h44tj11t45m4ah33q700000l3ep2021a200t220301012040a0i320j2040007f00100r20c100g3208000011531000s520201036020t900207l1101n311000000321210090a12634b910201l22001051020120011020000e1i520701020000n10h5001050000010000010001re0010323010001r11i520701000001q6000000107l600000230004gg12080001lc62027111n1000411230000000000t820321030030r10090n205001380800501020n2050c0001mc00002000000nc006150000i5l1100600010001q753011160000i441111000000rl11019100000c1h201043102n10ltk1f006eoss2b0202gs94r36si2033ku101m373h50b1rqs4113sli1t22m1uh2435070726u13k92rr7m34h380e0m141egl561g1261114k60ga6nn10t33sv13uf1jf0207150qt16t66pr60n23k6bf1f0e1l29g71c1e029j1n30p11ea0k1028139m16rb2v621122i52213040tf1u3b51n17127c0232041d120819052c28220104060306l5g3l4572g130152cqdl2c3o37a5o27u11c32d9m2cu2191obo57eh9gg1tv3' + '090001000h1v5g2903000hl2v6jh16ng1s210111110o35aa10u2k1g10l6600152113h2010u1q1r5aq3890o13181214r22k3158q2n1000v100h1k20000001207030001160721k171001h1j11610332010010321210090411021c1c02100o301201412230u1130b100h1j116111420120411001100k11l10051001h1j11611142011000321210072411021d0g10r3000000132120090o200200g1j11f2012031213711271t1001o301001000001121371b1f0c101h1l2011001031212000080a1t1001m40400102101600i11t301006c7i601008b6p41r10101041h3d0004115a1j290p3q223000511010100g1501010302003172030c0010016000e0i6v50n40n5tl12ir33s12t11u11cj30100060700010a90t220000l71i20m7203010241000502rd101004k300000006101027050920g3t22bk1300616g209030207u18c1000t10003010501b2o20000102000004g37070101o921c000640602001mcv3rg20000001o16d0c0m1fg6g2h30m10q55f1vm11c0ma0p1a42nc0ne1a090q3314902021204020112030318250105bb1201110i1021i15101131502141b15120511011k1155132040202510201010603060a1f02040104210b1g3290e0e0kn11vs12j11j3040pp12t80g6v1qi2500c0r51ktl7319g21g51gh103040n11010040j51i3f01q1h1d0m27p1a0000cs13200523j200010301010100v140018a4q2501010101c08000i1f12601011g3012215110u1a0001015000pcq181000100011000iji5fvc0u91g5bhg20ie0l94lo6002115303121s12240la1tj23h44tj11t45m4ah33q700000l3ep2021a200t220301012040a0i320j2040007f00100r20c100g3208000011531000s520201036020t900207l1101n311000000321210090a12634b910201l22001051020120011020000e1i520701020000n10h5001050000010000010001re0010323010001r11i520701000001q6000000107l600000230004gg12080001lc62027111n1000411230000000000t820321030030r10090n205001380800501020n2050c0001mc00002000000nc006150000i5l1100600010001q753011160000i441111000000rl11019100000c1h201043102n10ltk1f006eoss2b0202gs94r36si2033ku101m373h50b1rqs4113sli1t22m1uh2435070726u13k92rr7m34h380e0m141egl561g1261114k60ga6nn10t33sv13uf1jf0207150qt16t66pr60n23k6bf1f0e1l29g71c1e029j1n30p11ea0k1028139m16rb2v621122i52213040tf1u3b51n17127c0232041d120819052c28220104060306l5g3l4572g130152cqdl2c3o37a5o27u11c32d9m2cu2191obo57eh9gg1tv3' ); /** * Category of each range in {@link grapheme_data}, base36 digit per range. */ -export const grapheme_cats = '262122424333333393233393339333333333393393bf3b3b3b3b3f3f3bffff33b3bb33ff3f33b3b3333333bffff3b33bb33f33bffff33b3bb33ff3f33b3bbb333b3ff33b333f33b3b3b3b3333b3bf33b3bb39333b33b33b3b3b333b333333b3b33333f3b33b3ffb3f3fff3f3b33f35dc33333f3b3b3b33323333b3bb3b33b3fb3b3333b33333bff3b3f33bfb3b3ff3b3b3b3b3b333b333b3323e22442344444444444444444444444444444444444444444444444444444444444444444444444444444443333333333b3b3bb33333b353bff3b3b3b3f3ff3b3b333bfff3f33333fb3bb3fb3b3bb33232333f333fff333333333b3b3333bb3b39393f3b3fbf33bb3b393b3b3b3333b33b33b3bbb33b33ffff3b3333bb3933b3b3b333b3b3b3b3b33b3b3b33b3b3b33b3b33b33b3b3b3fffff3bb39b9b3b33b3bf3f3b33f3b3f93b33b3b3bb33b33b3b3b3333393b3b3b33b39bffb3b332333b333dd3b3333233332333333333333333333333333333444444444444a4444444444444344444444444444444444444444444444444444444444'; +export const grapheme_cats = '262122424333333393233393339333333333393393bf3b3b3b3b3f3f3bffff33b3bb33ff3f33b3b3333333bffff3b33bb33f33bffff33b3bb33ff3f33b3bbb333b3ff33b333f33b3b3b3b3333b3bf33b3bb39333b33b33b3b3b333b333333b3b33333f3b33b3ffb3f3fff3f3b33f35dc33333f3b3b3b33323333b3bb3b33b3fb3b3333b33333bff3b3f33bfb3b3ff3b3b3b3b3b333b333b3323e224423444444444444444444444444444444444444444444444444444444444444444444444444444444433334433333333b3b3bb33333b353bff3b3b3b3f3ff3b3b333bfff3f33333fb3bb3fb3b3bb33232333f333fff333333333b3b3333bb3b39393f3b3fbf33bb3b393b3b3b3333b33b33b3bbb33b33ffff3b3333bb3933b3b3b333b3b3b3b3b33b3b3b33b3b3b33b3b33b33b3b3b3fffff3bb39b9b3b33b3bf3f3b33f3b3f93b33b3b3bb33b33b3b3b3333393b3b3b33b39bffb3b332333b333dd3b3333233332333333333333333333333333333444444444444a4444444444444344444444444444444444444444444444444444444444'; /** * UAX #29 pair-wise rules for extended grapheme cluster boundaries, - * evaluated into a 16x16 decision table indexed by - * `catBefore << 4 | catAfter`. + * evaluated into a 16x16 mask table indexed by `catBefore << 4 | catAfter`. * - * - 0: boundary (GB999 and friends) - * - 1: no boundary + * A boundary exists iff the mask shares no bit with the runtime's packed sequence state. + * + * - 0: boundary (GB999 and friends), no state bit can match + * - 1: no boundary; bit 0 is always set in the state * - 2: GB12/GB13, no boundary iff odd run of RI precedes - * - 3: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* - * - 4: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes + * - 4: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* + * - 8: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes * * See `buildGraphemePairTable` in scripts/unicode.js for the rules. */ -export const grapheme_pairs = '0001000000010010000000100000000000000000000000000001000000010014000100000001001000010101100101100000000000000000000100000001111000010000000110101001110111111111000100000021001000010000000100100001000000011010000100000001111000013000000100140001000000010010'; +export const grapheme_pairs = '0001000000010010000000100000000000000000000000000001000000010018000100000001001000010101100101100000000000000000000100000001111000010000000110101001110111111111000100000021001000010000000100100001000000011010000100000001111000014000000100180001000000010010'; diff --git a/src/core.js b/src/core.js index 2944092..6b71f95 100644 --- a/src/core.js +++ b/src/core.js @@ -31,14 +31,18 @@ */ /** - * @template {number} [T=number] + * Decode {@link UnicodeDataEncoding} and hand every range to `emit`. + * + * Callers build their own index from the callback, so decoding never materializes an intermediate array of ranges; + * on the grapheme table that is ~800 short-lived tuples per module load. + * * @param {UnicodeDataEncoding} data - * @param {string} [cats=''] - * @returns {Array>} + * @param {string} cats base36 category digit per range, or `''` for none + * @param {(from: number, to: number, cat: number) => void} emit + * @return {void} */ -export function decodeUnicodeData(data, cats = '') { - let buf = /** @type {Array>} */([]) - , len = data.length +export function decodeUnicodeData(data, cats, emit) { + let len = data.length , i = 0 , j = 0 , p = -1; @@ -53,10 +57,8 @@ export function decodeUnicodeData(data, cats = '') { }; while (i < len) { let from = p + 1 + read(); - p = from + read(); - buf.push([from, p, /** @type {T} */ (cats ? parseInt(cats[j++], 36) : 0)]); + emit(from, p = from + read(), cats ? parseInt(cats[j++], 36) : 0); } - return buf; } /** @@ -113,12 +115,15 @@ export function findUnicodeRangeCategory(cp, starts, ends) { * @return {UnicodeRangeTable} */ export function decodeUnicodeFlatData(data) { - let ranges = decodeUnicodeData(data) - , starts = new Uint32Array(ranges.length) - , ends = new Uint32Array(ranges.length); - for (let i = 0; i < ranges.length; i++) { - starts[i] = ranges[i][0]; - ends[i] = ranges[i][1] << 5 | 1; - } - return [starts, ends]; + // Every range takes at least two characters, + // so half the encoded length is a safe upper bound for the range count. + let cap = data.length >> 1 + , starts = new Uint32Array(cap) + , ends = new Uint32Array(cap) + , n = 0; + decodeUnicodeData(data, '', (from, to) => { + starts[n] = from; + ends[n++] = to << 5 | 1; + }); + return [starts.slice(0, n), ends.slice(0, n)]; } diff --git a/src/grapheme.js b/src/grapheme.js index dec4434..ab174e2 100644 --- a/src/grapheme.js +++ b/src/grapheme.js @@ -24,32 +24,31 @@ import { export { GraphemeCategory }; -// Repeated bounds are `let` bindings on purpose; minifiers inline -// `const` numbers into every use site, but keep `let` shared. -let BMP_MAX = 0xFFFF; -let T1_MIN = 0xA000; -let T2_MIN = 0x1F000; - -// Direct category lookup tables for the hot Unicode regions, and a flat -// binary-search tail for everything rare. +// Every bound below is spelled out as a literal on purpose. // -// - T0: 0x0000-0x2FFF (ASCII, Latin, Greek, Cyrillic, Semitic, Indic, SEA, ...) -// - 0x3000-0x9FFF (CJK): only 12 non-Any code points, computed inline -// - T1: 0xA000-0xABFF +// A shared module-scope binding saves a few bundled bytes, but it is a mutable context slot +// that engines cannot fold into the comparison; hoisting the hottest one into a `let` measured up to 25% slower on the count loop. + +// Direct category lookup tables for the hot Unicode regions, and a flat binary-search tail for everything rare. +// +// - T0: 0x0000-0x309F (ASCII, Latin, Greek, Cyrillic, Semitic, Indic, SEA, CJK punctuation and kana; +// the table ends right after the last combining kana mark so that the CJK blocks need no lookup at all) +// - 0x30A0-0xA65F (CJK, Yi, Lisu, Vai): only U+3297 and U+3299 are not `Any` +// - T1: 0xA660-0xABFF // - 0xAC00-0xD7A3 (Hangul syllables): LV or LVT computed at runtime -// - 0xD7A4-0xFDFF (Jamo Ext-B, surrogates, private use): computed inline -// - 0xFE00-0xFE0F (variation selectors): computed inline // - T2: 0x1F000-0x1FAFF (emoji) -// - 0xE0000-0xE0FFF (tags, VS supplement): computed inline +// - 0xFE00-0xFE0F (variation selectors): computed inline +// - 0xD7A4-0xFDFF (Jamo Ext-B, surrogates, private use): {@link catRare} +// - 0xE0000-0xE0FFF (tags, VS supplement): {@link catRare} // - everything else: binary search over the range tail (TAIL_S/TAIL_E) // // The `Indic_Conjunct_Break=Consonant` property is folded into the category // space as the 16th category (15) since it never overlaps other categories. // It shares break semantics with `Any` and never escapes to the public API. // -// Total index size: ~21KiB of flat typed arrays, no retained JS objects. -const T0 = new Uint8Array(0x3000); -const T1 = new Uint8Array(0xC00); +// Total index size: ~19KiB of flat typed arrays, no retained JS objects. +const T0 = new Uint8Array(0x30A0); +const T1 = new Uint8Array(0x5A0); const T2 = new Uint8Array(0xB00); /** @type {Uint32Array} Range starts of the binary-search tail */ let TAIL_S; @@ -63,21 +62,21 @@ let TAIL_E; /** @type {number} */ to, /** @type {number} */ cat, ) => { - let hi = lo + t.length - 1; - for (let cp = from < lo ? lo : from, top = to > hi ? hi : to; cp <= top; cp++) { - t[cp - lo] = cat; + let end = to - lo + 1, len = t.length; + if (end > 0 && from - lo < len) { + t.fill(cat, from > lo ? from - lo : 0, end > len ? len : end); } }; let starts = new Uint32Array(320), ends = new Uint32Array(320), n = 0; - for (let [from, to, cat] of decodeUnicodeData(grapheme_data, grapheme_cats)) { + decodeUnicodeData(grapheme_data, grapheme_cats, (from, to, cat) => { fill(T0, 0, from, to, cat); - fill(T1, T1_MIN, from, to, cat); - fill(T2, T2_MIN, from, to, cat); - if (to >= 0xFE10 && !(from >= T2_MIN && to <= 0x1FAFF)) { + fill(T1, 0xA660, from, to, cat); + fill(T2, 0x1F000, from, to, cat); + if (to >= 0xFE10 && !(from >= 0x1F000 && to <= 0x1FAFF)) { starts[n] = from; ends[n++] = to << 5 | cat; } - } + }); TAIL_S = starts.slice(0, n); TAIL_E = ends.slice(0, n); } @@ -91,19 +90,30 @@ let TAIL_E; * @return {number} category number, {@link GraphemeCategoryNum} or 15 (`InCB=Consonant`) */ function cat(cp) { - if (cp < 0x3000) return T0[cp]; - // CJK: 0x3000-0x9FFF - if (cp < T1_MIN) { - if (cp < 0x3030) return cp >= 0x302A ? 3 : 0; - if (cp < 0x309B) { - if (cp === 0x3030 || cp === 0x303D) return 4; - return cp >= 0x3099 ? 3 : 0; - } - return (cp === 0x3297 || cp === 0x3299) ? 4 : 0; - } - if (cp < 0xAC00) return T1[cp - T1_MIN]; + if (cp < 0x30A0) return T0[cp]; + // CJK through Vai: U+3297 and U+3299 are the only two that are not `Any` + if (cp < 0xA660) return cp === 0x3297 || cp === 0x3299 ? 4 : 0; + if (cp < 0xAC00) return T1[cp - 0xA660]; // Hangul syllables: 0xAC00-0xD7A3, LV at every 28th if (cp < 0xD7A4) return (cp - 0xAC00) % 28 ? 8 : 7; + // Emoji: 0x1F000-0x1FAFF + if (cp >= 0x1F000 && cp < 0x1FB00) return T2[cp - 0x1F000]; + // Variation selectors: 0xFE00-0xFE0F + if (cp >>> 4 === 0xFE0) return 3; + return catRare(cp); +} + +/** + * Cold half of {@link cat}. + * Kept out of line so that the hot half stays well inside V8's 460-bytecode inlining limit. + * + * As a single function the ladder compiled to 453 bytecodes, + * one comparison away from a cliff that cost 15-45% on every segmentation loop once `cat` stopped being inlined. + * + * @param {number} cp in 0xD7A4-0xDFFF, 0xE000-0xFDFF, 0xFE10-0x1EFFF or 0x1FB00+ + * @return {number} category number + */ +function catRare(cp) { // Hangul Jamo Extended-B, unassigned and surrogates as Any if (cp < 0xE000) { if (cp <= 0xD7C6) return cp >= 0xD7B0 ? 13 : 0; @@ -111,10 +121,6 @@ function cat(cp) { } // Private use if (cp < 0xFE00) return cp === 0xFB1E ? 3 : 0; - // Variation selectors - if (cp < 0xFE10) return 3; - // Emoji: 0x1F000-0x1FAFF - if (cp >= T2_MIN && cp < 0x1FB00) return T2[cp - T2_MIN]; // Tags and variation selectors supplement: 0xE0000-0xE0FFF if (cp >= 0xE0000) { if (cp > 0xE0FFF) return 0; @@ -124,13 +130,16 @@ function cat(cp) { return findUnicodeRangeCategory(cp, TAIL_S, TAIL_E); } -// Boundary decision table for category pairs, `PAIR[catBefore << 4 | catAfter]` +// Boundary mask table for category pairs, `PAIR[catBefore << 4 | catAfter]`. // -// - 0: boundary (GB999 and friends) -// - 1: no boundary +// A boundary exists iff the mask shares no bit with the packed sequence state, +// so the whole decision is `!(st & PAIR[...])`: +// +// - 0: boundary (GB999 and friends), no state bit can match +// - 1: no boundary; bit 0 is always set in the state // - 2: GB12/GB13, no boundary iff odd run of RI precedes -// - 3: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* -// - 4: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes +// - 4: GB11, no boundary iff the ZWJ was preceded by ExtPic Extend* +// - 8: GB9c, no boundary iff InCB Consonant [Extend Linker]* Linker [Extend Linker]* precedes const PAIR = Uint8Array.from(grapheme_pairs); /** @@ -163,71 +172,44 @@ function isLinker(cp) { || cp === 0x11F42; // Kawi Conjoiner } -/** - * State transition on consuming an Extend code point (category 3). - * The cp-dependent ZWNJ / `isLinker` branches are extracted here so - * that {@link nextState} stays a compact switch. - * - * @param {number} st packed state - * @param {number} cp the consumed Extend code point - * @return {number} next packed state - */ -function nextExtend(st, cp) { - if (st & 24) { - if (cp === 0x200C) return st & 6; // ZWNJ has InCB=None - if ((st & 24) === 16 || isLinker(cp)) return (st & 6) | 16; - return (st & 6) | 8; - } - return st & 6; -} - -// Sequence state, packed in a small int: +// Sequence state, packed in a small int. The bits a boundary rule can ask +// about are laid out so that each {@link PAIR} mask selects exactly one: // -// bit 0 : odd run of Regional_Indicator immediately precedes (GB12, GB13) -// bit 1 : ExtPic Extend* immediately precedes (GB11) -// bit 2 : the last consumed ZWJ was preceded by ExtPic Extend* (GB11) -// bit 3-4 : InCB state; 01 = Consonant [Extend Linker]* precedes, -// 10 = it also contains a Linker (GB9c) +// bit 0 (1) : always set, so the no-boundary mask always matches +// bit 1 (2) : odd run of Regional_Indicator immediately precedes (GB12, GB13) +// bit 2 (4) : the last consumed ZWJ was preceded by ExtPic Extend* (GB11) +// bit 3 (8) : the InCB Consonant run contains a Linker (GB9c) +// bit 4 (16) : ExtPic Extend* immediately precedes (feeds bit 2 on ZWJ) +// bit 5 (32) : InCB Consonant [Extend Linker]* precedes // -// It is a pure function of the consumed code point sequence, so it carries -// across segment boundaries without any reset. Non-stateful categories -// reset the state to 0 inside {@link nextState}. +// It is a pure function of the consumed code point sequence, +// so it carries across segment boundaries without any reset. +// Non-stateful categories reset the state to 1 inside {@link nextState}. /** - * Resolve a pair rule to a boundary decision. - * - * Each entry in {@link PAIR} encodes *how* to decide a boundary between - * two adjacent categories, not the decision itself: - * - * 0 -> always a boundary (break) - * 1 -> never a boundary (no-break) - * 2 -> boundary unless an odd number of Regional_Indicators precede (GB12/GB13) - * 3 -> boundary unless preceded by ExtPic Extend* (GB11) - * 4 -> boundary unless an InCB Linker has been seen in the run (GB9c) - * - * Rules 2-4 consult the packed sequence state `st` that was captured - * *before* the state transition on `catAfter`. + * State transition on consuming an Extend code point (category 3) that continues an InCB Consonant run. + * The cp-dependent ZWNJ / `isLinker` branches are extracted here so that {@link nextState} stays small enough to inline. * * @param {number} st packed state - * @param {number} pairRule the pair-rule value from {@link PAIR} - * @return {boolean} whether a boundary exists + * @param {number} cp the consumed Extend code point + * @return {number} next packed state */ -function isBoundary(st, pairRule) { - switch (pairRule) { - case 0: return true; - case 1: return false; - case 2: return !(st & 1); - case 3: return !(st & 4); - default: return (st & 24) !== 16; - } +function nextExtend(st, cp) { + if (cp === 0x200C) return st & 21; // ZWNJ has InCB=None + if (st & 8 || isLinker(cp)) return st & 21 | 40; + return st & 21 | 32; } /** * State transition on consuming a code point. * - * Extend (cat 3) is cp-dependent and delegates to {@link nextExtend}; - * the remaining stateful categories (4, 10, 14, 15) are inline; - * everything else resets to 0. + * Extend (cat 3) is cp-dependent within an InCB Consonant run and delegates to {@link nextExtend}; + * the remaining stateful categories (4, 10, 14, 15) are inline; everything else resets to 1. + * + * @remarks + * > Keep this body at or below 99 bytecodes (`node --print-bytecode -print-bytecode-filter=nextState`): + * > Maglev refuses to inline anything from 100 bytecodes up, and the resulting call per code point measured 8-26% slower there. + * > That is also why the tests are an `if` chain rather than a `switch`, which spends 3 extra bytecodes on the discriminant. * * @param {number} st packed state * @param {number} c category of the consumed code point @@ -235,14 +217,12 @@ function isBoundary(st, pairRule) { * @return {number} next packed state */ function nextState(st, c, cp) { - switch (c) { - case 3: return st ? nextExtend(st, cp) : 0; - case 4: return 2; // Extended_Pictographic - case 10: return (st & 1) ^ 1; // Regional_Indicator - case 14: return (st & 2) << 1 | (st & 24); // ZWJ - case 15: return 8; // InCB=Consonant - default: return 0; - } + if (c === 3) return st & 32 ? nextExtend(st, cp) : st & 21; + if (c === 4) return 17; // Extended_Pictographic + if (c === 10) return (st & 2) ^ 3; // Regional_Indicator + if (c === 14) return (st & 16) >> 2 | st & 41; // ZWJ + if (c === 15) return 33; // InCB=Consonant + return 1; } /** @@ -259,13 +239,13 @@ export function* graphemeSegments(input) { if (len === 0) return; let cp = /** @type {number} */ (input.codePointAt(0)); - let cursor = cp > BMP_MAX ? 2 : 1; + let cursor = cp > 0xFFFF ? 2 : 1; /** Category of the last consumed code point */ let catBefore = cat(cp); - /** Packed sequence state */ - let st = nextState(0, catBefore, cp); + /** Packed sequence state, seeded with the always-set bit 0 */ + let st = nextState(1, catBefore, cp); /** Start index of the current segment */ let index = 0; @@ -279,7 +259,7 @@ export function* graphemeSegments(input) { while (cursor < len) { cp = /** @type {number} */ (input.codePointAt(cursor)); let catAfter = cat(cp); - let boundary = isBoundary(st, PAIR[catBefore << 4 | catAfter]); + let boundary = !(st & PAIR[catBefore << 4 | catAfter]); st = nextState(st, catAfter, cp); @@ -296,7 +276,7 @@ export function* graphemeSegments(input) { hd = cp; catBegin = catAfter; } - cursor += cp > BMP_MAX ? 2 : 1; + cursor += cp > 0xFFFF ? 2 : 1; catBefore = catAfter; } @@ -321,13 +301,13 @@ export function countGraphemes(input) { if (len === 0) return 0; let cp = /** @type {number} */ (input.codePointAt(0)); - let cursor = cp > BMP_MAX ? 2 : 1; + let cursor = cp > 0xFFFF ? 2 : 1; /** Category of the last consumed code point */ let catBefore = cat(cp); - /** Packed sequence state */ - let st = nextState(0, catBefore, cp); + /** Packed sequence state, seeded with the always-set bit 0 */ + let st = nextState(1, catBefore, cp); /** The segment being scanned counts, whether or not a boundary follows */ let count = 1; @@ -335,12 +315,11 @@ export function countGraphemes(input) { while (cursor < len) { cp = /** @type {number} */ (input.codePointAt(cursor)); let catAfter = cat(cp); - let boundary = isBoundary(st, PAIR[catBefore << 4 | catAfter]); + count += +!(st & PAIR[catBefore << 4 | catAfter]); st = nextState(st, catAfter, cp); - if (boundary) count += 1; - cursor += cp > BMP_MAX ? 2 : 1; + cursor += cp > 0xFFFF ? 2 : 1; catBefore = catAfter; } @@ -366,7 +345,37 @@ export { * [...splitGraphemes('abc')] // => ['a', 'b', 'c'] */ export function* splitGraphemes(input) { - for (let s of graphemeSegments(input)) yield s.segment; + let len = input.length; + if (len === 0) return; + + let cp = /** @type {number} */ (input.codePointAt(0)); + let cursor = cp > 0xFFFF ? 2 : 1; + + /** Category of the last consumed code point */ + let catBefore = cat(cp); + + /** Packed sequence state, seeded with the always-set bit 0 */ + let st = nextState(1, catBefore, cp); + + /** Start index of the current segment */ + let index = 0; + + while (cursor < len) { + cp = /** @type {number} */ (input.codePointAt(cursor)); + let catAfter = cat(cp); + let boundary = !(st & PAIR[catBefore << 4 | catAfter]); + + st = nextState(st, catAfter, cp); + + if (boundary) { + yield input.slice(index, cursor); + index = cursor; + } + cursor += cp > 0xFFFF ? 2 : 1; + catBefore = catAfter; + } + + yield input.slice(index); } /** @@ -391,13 +400,13 @@ export function collectGraphemes(input) { if (len === 0) return result; let cp = /** @type {number} */ (input.codePointAt(0)); - let cursor = cp > BMP_MAX ? 2 : 1; + let cursor = cp > 0xFFFF ? 2 : 1; /** Category of the last consumed code point */ let catBefore = cat(cp); - /** Packed sequence state */ - let st = nextState(0, catBefore, cp); + /** Packed sequence state, seeded with the always-set bit 0 */ + let st = nextState(1, catBefore, cp); /** Start index of the current segment */ let index = 0; @@ -405,7 +414,7 @@ export function collectGraphemes(input) { while (cursor < len) { cp = /** @type {number} */ (input.codePointAt(cursor)); let catAfter = cat(cp); - let boundary = isBoundary(st, PAIR[catBefore << 4 | catAfter]); + let boundary = !(st & PAIR[catBefore << 4 | catAfter]); st = nextState(st, catAfter, cp); @@ -413,7 +422,7 @@ export function collectGraphemes(input) { result.push(input.slice(index, cursor)); index = cursor; } - cursor += cp > BMP_MAX ? 2 : 1; + cursor += cp > 0xFFFF ? 2 : 1; catBefore = catAfter; } @@ -421,13 +430,12 @@ export function collectGraphemes(input) { return result; } -// Keep one live segmenter and its result reachable so their hidden classes -// stay strongly referenced. Otherwise a major GC while no segmenter is -// alive clears the maps embedded weakly in JIT-optimized code, and the -// next run pays deoptimization and re-learning costs. +// Keep one live segmenter and its result reachable so their hidden classes stay strongly referenced. +// Otherwise a major GC while no segmenter is alive clears the maps embedded weakly in JIT-optimized code, +// and the next run pays deoptimization and re-learning costs. // -// The instances are stashed on an always-retained module object, since -// unreferenced module-scope bindings do not survive module evaluation. +// The instances are stashed on an always-retained module object, +// since unreferenced module-scope bindings do not survive module evaluation. { let keep = graphemeSegments('_'); // @ts-ignore intended expando From 7cce20dd97c9388394e5b1548eb30e09b2010efc Mon Sep 17 00:00:00 2001 From: Hyeseong Kim Date: Mon, 27 Jul 2026 03:15:23 +0900 Subject: [PATCH 2/4] add perf records measured on my Mac --- ...727-apple_m4_pro-macos_26.5-bun_1.3.14.txt | 265 ++++++++++++++++++ ...4_pro-macos_26.5-chrome_150.0.7871.186.txt | 197 +++++++++++++ ...pple_m4_pro-macos_26.5-firefox_154.0b1.txt | 197 +++++++++++++ ...-apple_m4_pro-macos_26.5-nodejs_26.5.0.txt | 265 ++++++++++++++++++ ...-apple_m4_pro-macos_26.5-safari_26.5.2.txt | 197 +++++++++++++ .../grapheme/_records/report.generated.html | 4 +- 6 files changed, 1123 insertions(+), 2 deletions(-) create mode 100644 benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-bun_1.3.14.txt create mode 100644 benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-chrome_150.0.7871.186.txt create mode 100644 benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-firefox_154.0b1.txt create mode 100644 benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-nodejs_26.5.0.txt create mode 100644 benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-safari_26.5.2.txt diff --git a/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-bun_1.3.14.txt b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-bun_1.3.14.txt new file mode 100644 index 0000000..944be63 --- /dev/null +++ b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-bun_1.3.14.txt @@ -0,0 +1,265 @@ +clk: ~4.15 GHz +cpu: Apple M4 Pro +runtime: bun 1.3.14 (arm64-darwin) + +benchmark avg (min … max) p75 / p99 (min … top 1%) +------------------------------------------------------------- ------------------------------- +• Lorem ipsum (ascii) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.04 µs/iter 2.04 µs ▆█ + (2.00 µs … 2.32 µs) 2.15 µs ▅▇██▇▆▄▃▆▄▂▂▁▁▁▁▁▁▁▁▁ + gc(594.54 µs … 898.75 µs) 105.56 b ( 0.00 b… 15.49 kb) + +graphemer 17.83 µs/iter 18.25 µs █▂ + (15.71 µs … 60.46 µs) 30.38 µs ▃██▄▅▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ + gc(292.50 µs … 17.05 ms) 90.56 b ( 0.00 b… 96.00 kb) + +grapheme-splitter 69.51 µs/iter 70.13 µs ██▃▂ + (52.00 µs … 128.54 µs) 123.42 µs ▃█████▄▃▂▁▃▂▂▂▁▂▂▂▁▃▂ + gc(313.58 µs … 320.61 ms) 3.47 kb ( 0.00 b…128.00 kb) + +@formatjs/intl-segmenter 56.55 µs/iter 58.50 µs ▄█ ▅▄ + (49.29 µs … 101.17 µs) 79.63 µs ▃██████▅▆▃▃▂▂▂▂▂▁▁▁▁▁ + gc(311.00 µs … 134.75 ms) 1.23 kb ( 0.00 b…752.00 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 12.64 µs/iter 12.71 µs ▇█▄ + (10.50 µs … 54.83 µs) 24.92 µs ▄███▅▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁ + gc(313.88 µs … 691.83 µs) 50.46 b ( 0.00 b… 16.00 kb) + +Intl.Segmenter 4.15 µs/iter 4.22 µs ▅ █ ▅ + (3.98 µs … 4.34 µs) 4.33 µs ▄▄▄▆▄▄████▆▃▄██▆▄▄▄▁▄ + gc( 1.70 ms … 2.39 ms) 65.26 b ( 0.00 b… 4.63 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.04 µs + graphemer ┤■■■■■■■■ 17.83 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 69.51 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■ 56.55 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 12.64 µs + Intl.Segmenter ┤■ 4.15 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.04x faster than Intl.Segmenter + 6.2x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 8.75x faster than graphemer + 27.75x faster than @formatjs/intl-segmenter + 34.11x faster than grapheme-splitter + +• Emojis +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 824.86 ns/iter 834.07 ns █▄▄ ▂ + (791.15 ns … 880.29 ns) 871.35 ns ▂▃▄▅▇███████▇▄▆▂▃▂▂▁▂ + gc(435.58 µs … 809.13 µs) 0.07 b ( 0.00 b… 20.00 b) + +graphemer 4.96 µs/iter 4.99 µs ▂ ▂▆█ █ ▆█▄ + (4.87 µs … 5.13 µs) 5.12 µs ▃█▃███▇█▅███▅▇▁▁▁▁▃▃▃ + gc(701.38 µs … 1.08 ms) 52.67 b ( 0.00 b… 3.35 kb) + +grapheme-splitter 17.56 µs/iter 17.67 µs ▃ ▃ █ ▃ + (17.29 µs … 17.95 µs) 17.95 µs █▁█▆▆▆▁█▁▁▁█▆▁▁▁▁▁▁▆▆ + gc(703.17 µs … 856.83 µs) 0.00 b ( 0.00 b… 0.00 b) + +@formatjs/intl-segmenter 20.21 µs/iter 20.52 µs █ █ █ + (19.76 µs … 20.79 µs) 20.67 µs █▁█▁▁███▁▁▁██▁▁▁▁█▁▁█ + gc( 1.81 ms … 2.05 ms) 57.41 b ( 0.00 b…976.00 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 6.21 µs/iter 6.29 µs █ + (5.17 µs … 22.67 µs) 11.67 µs ▄███▄▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁ + gc(327.63 µs … 679.13 µs) 0.00 b ( 0.00 b… 0.00 b) + +Intl.Segmenter 1.54 µs/iter 1.56 µs ▂▂▂ ▇▅█ ▂ + (1.50 µs … 1.64 µs) 1.63 µs ▇██████████▇▄▃▅▃▃▂▁▂▂ + gc( 1.49 ms … 2.11 ms) 0.00 b ( 0.00 b… 0.00 b) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 824.86 ns + graphemer ┤■■■■■■■ 4.96 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 17.56 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 20.21 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■ 6.21 µs + Intl.Segmenter ┤■ 1.54 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.87x faster than Intl.Segmenter + 6.02x faster than graphemer + 7.52x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 21.29x faster than grapheme-splitter + 24.51x faster than @formatjs/intl-segmenter + +• Hindi +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.55 µs/iter 2.58 µs ▆ ▅▅ █▅▃ ▅▅█▃ + (2.47 µs … 2.64 µs) 2.62 µs █▄██▆██████████████▃▃ + gc( 1.09 ms … 1.46 ms) 0.03 b ( 0.00 b… 4.00 b) + +graphemer 17.60 µs/iter 17.68 µs █ ██ + (17.38 µs … 17.87 µs) 17.75 µs █▁█▁▁██▁█▁█▁▁█▁███▁██ + gc( 1.06 ms … 1.28 ms) 43.00 b ( 0.00 b…624.00 b) + +grapheme-splitter 34.87 µs/iter 35.21 µs █ █ + (33.96 µs … 36.65 µs) 35.55 µs ██▁▁▁▁█▁█▁▁█▁▁▁██▁█▁█ + gc( 1.10 ms … 1.26 ms) 0.00 b ( 0.00 b… 0.00 b) + +@formatjs/intl-segmenter 63.70 µs/iter 64.59 µs █ + (62.54 µs … 65.32 µs) 64.91 µs █▁█▁█▁███▁▁▁█▁▁▁▁██▁█ + gc( 2.48 ms … 2.65 ms) 30.18 b ( 0.00 b…328.00 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 10.97 µs/iter 11.01 µs █▄ + (10.85 µs … 11.15 µs) 11.12 µs ▅▅▁██▅██▁█▅▅▅▅█▁▁▁▁▅▅ + gc( 4.04 ms … 5.07 ms) 133.33 b ( 0.00 b… 3.50 kb) + +Intl.Segmenter 3.60 µs/iter 3.64 µs █ + (3.48 µs … 3.78 µs) 3.73 µs ▂▄▅▂▅▃▄█▆▂▅▆▅▆▂▂▃▆▃▁▃ + gc( 1.63 ms … 2.12 ms) 0.05 b ( 0.00 b… 4.00 b) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.55 µs + graphemer ┤■■■■■■■■ 17.60 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■ 34.87 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 63.70 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 10.97 µs + Intl.Segmenter ┤■ 3.60 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.41x faster than Intl.Segmenter + 4.31x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 6.91x faster than graphemer + 13.69x faster than grapheme-splitter + 25.01x faster than @formatjs/intl-segmenter + +• Demonic characters +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 441.81 ns/iter 447.14 ns ██▅▃▄ ▂ + (423.29 ns … 475.70 ns) 467.44 ns ▁▂▂▇██████▇█▄▇▅▅▂▃▂▂▁ + gc(367.88 µs … 589.58 µs) 0.00 b ( 0.00 b… 0.00 b) + +graphemer 7.97 µs/iter 8.02 µs ▂ █ ▂ ▂ + (7.83 µs … 8.18 µs) 8.12 µs ▄▄█▄▄▇█▇█▇▄▄▇█▇▇▁▁▇▇▄ + gc(806.00 µs … 1.00 ms) 11.43 b ( 0.00 b…476.00 b) + +grapheme-splitter 7.16 µs/iter 7.21 µs ▂ ▂ █▂ ▂ + (7.04 µs … 7.37 µs) 7.34 µs ▇▃█▅█▃▃██▇▃▁█▃▁▇▁▁▃▃▃ + gc(820.08 µs … 1.13 ms) 0.00 b ( 0.00 b… 0.00 b) + +@formatjs/intl-segmenter 73.10 µs/iter 76.50 µs █ ▃ ▃ + (65.50 µs … 96.71 µs) 86.67 µs ▃█▇▄▅█▆▄███▇▆▄▃▃▃▂▁▂▁ + gc(337.25 µs … 714.38 µs) 0.00 b ( 0.00 b… 0.00 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 2.15 µs/iter 2.18 µs ▆▃█▇▃▂▆▄▄▄ █▂ + (2.09 µs … 2.25 µs) 2.24 µs ▄██████████▇▇██▇▅▂▄▂▂ + gc(696.17 µs … 951.50 µs) 0.25 b ( 0.00 b… 36.00 b) + +Intl.Segmenter 717.64 ns/iter 726.52 ns ▄▄█▆▃ + (681.83 ns … 848.61 ns) 806.65 ns ▃███████▇▃▅▂▃▂▃▁▃▂▂▁▁ + gc( 1.24 ms … 2.05 ms) 0.03 b ( 0.00 b… 4.00 b) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 441.81 ns + graphemer ┤■■■■ 7.97 µs + grapheme-splitter ┤■■■ 7.16 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 73.10 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■ 2.15 µs + Intl.Segmenter ┤ 717.64 ns + └ ┘ + +summary + unicode-segmenter/grapheme + 1.62x faster than Intl.Segmenter + 4.87x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 16.2x faster than grapheme-splitter + 18.04x faster than graphemer + 165.45x faster than @formatjs/intl-segmenter + +• Tweet text (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 4.16 µs/iter 4.17 µs ▃▃▃ █ + (4.09 µs … 4.42 µs) 4.30 µs ▄███▂▄█▆▄▇▂▁▁▂▄▃▁▃▁▁▂ + gc( 1.52 ms … 1.84 ms) 0.00 b ( 0.00 b… 0.00 b) + +graphemer 25.03 µs/iter 25.05 µs █ █ + (24.88 µs … 25.30 µs) 25.24 µs █▁▁██▁▁█▁█▁█▁▁▁▁▁▁▁▁█ + gc( 1.05 ms … 1.19 ms) 2.29 b ( 0.00 b… 32.00 b) + +grapheme-splitter 56.23 µs/iter 56.66 µs █ + (55.12 µs … 58.37 µs) 57.28 µs ██▁▁██▁▁███▁▁▁██▁▁▁▁█ + gc( 1.15 ms … 1.37 ms) 0.00 b ( 0.00 b… 0.00 b) + +@formatjs/intl-segmenter 84.09 µs/iter 87.04 µs ▆█▂ + (72.79 µs … 106.58 µs) 97.25 µs ▂▃▄▇▅▅▅▆▅█████▄▃▂▂▁▁▁ + gc(344.83 µs … 662.96 µs) 0.00 b ( 0.00 b… 0.00 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 15.64 µs/iter 15.66 µs ██ █ + (15.49 µs … 15.82 µs) 15.81 µs █▁█▁████████▁▁█▁▁▁█▁█ + gc( 5.14 ms … 5.58 ms) 48.95 b ( 0.00 b… 1.00 kb) + +Intl.Segmenter 6.20 µs/iter 6.24 µs █ █ ▂ + (6.09 µs … 6.41 µs) 6.40 µs ▇▃▅█▅▅▅██▅█▇▁▁▅▁▁▁▁▃▃ + gc( 1.92 ms … 2.71 ms) 5.02 b ( 0.00 b…256.00 b) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 4.16 µs + graphemer ┤■■■■■■■■■ 25.03 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■ 56.23 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 84.09 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 15.64 µs + Intl.Segmenter ┤■ 6.20 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.49x faster than Intl.Segmenter + 3.76x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 6.02x faster than graphemer + 13.53x faster than grapheme-splitter + 20.23x faster than @formatjs/intl-segmenter + +• Code snippet (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 10.14 µs/iter 10.16 µs ▃▃ ▃ █ + (10.05 µs … 10.47 µs) 10.30 µs ██▁█▄▁▄█▄▄▄▁▁▆▁▁▁▁▁▄▆ + gc( 1.82 ms … 2.47 ms) 126.79 b ( 0.00 b… 4.04 kb) + +graphemer 58.94 µs/iter 59.20 µs █ █ + (57.99 µs … 61.49 µs) 59.61 µs █▁▁▁▁██▁██▁▁▁█▁█▁█▁▁█ + gc( 1.20 ms … 1.34 ms) 2.00 b ( 0.00 b… 24.00 b) + +grapheme-splitter 130.20 µs/iter 136.46 µs █▅ + (120.46 µs … 173.88 µs) 152.33 µs ▄████▆▄▃▄▅█▇▅▄▃▃▂▂▂▁▁ + gc(350.58 µs … 1.12 ms) 0.00 b ( 0.00 b… 0.00 b) + +@formatjs/intl-segmenter 182.92 µs/iter 189.00 µs ▃▅▄█▆▃▂ + (161.88 µs … 215.04 µs) 205.50 µs ▂▃▃▆▇██████████▇▅▃▃▂▁ + gc(353.96 µs … 751.71 µs) 0.00 b ( 0.00 b… 0.00 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 37.69 µs/iter 37.76 µs █ + (36.98 µs … 39.43 µs) 38.07 µs ██▁▁▁█▁▁██▁██▁█▁▁█▁▁█ + gc( 5.79 ms … 6.39 ms) 206.00 b ( 0.00 b… 2.40 kb) + +Intl.Segmenter 14.75 µs/iter 14.83 µs ▃ █▃ + (14.59 µs … 14.99 µs) 14.99 µs █▆▁▆▆██▆▆▆▁▁▆▆▁▆▁▆▁▁▆ + gc( 1.53 ms … 1.95 ms) 21.04 b ( 0.00 b…484.00 b) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 10.14 µs + graphemer ┤■■■■■■■■■■ 58.94 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■ 130.20 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 182.92 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 37.69 µs + Intl.Segmenter ┤■ 14.75 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.45x faster than Intl.Segmenter + 3.72x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 5.81x faster than graphemer + 12.84x faster than grapheme-splitter + 18.04x faster than @formatjs/intl-segmenter + +! = run with sudo to enable hardware counters diff --git a/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-chrome_150.0.7871.186.txt b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-chrome_150.0.7871.186.txt new file mode 100644 index 0000000..c089310 --- /dev/null +++ b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-chrome_150.0.7871.186.txt @@ -0,0 +1,197 @@ +clk: ~4.19 GHz +cpu: null +runtime: null (null) + +benchmark avg (min … max) p75 / p99 (min … top 1%) +------------------------------------------------------------- ------------------------------- +• Lorem ipsum (ascii) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.60 µs/iter 2.61 µs ▆ █ + (2.54 µs … 2.71 µs) 2.69 µs ▆▁▁█▁▁▁█▁▁█▁▁▇▁▁▁▅▁▁▂ +graphemer 65.62 µs/iter 100.00 µs █ + (0.00 ps … 400.00 µs) 300.00 µs █▁▁▁▁▁▁█▁▁▁▁▁▃▁▁▁▁▁▁▃ +grapheme-splitter 69.30 µs/iter 100.00 µs █ ▄ + (0.00 ps … 800.00 µs) 300.00 µs █▁▁▁▁▁▁█▁▁▁▁▁▃▁▁▁▁▁▁▂ +@formatjs/intl-segmenter 74.14 µs/iter 100.00 µs █ ▃ + (0.00 ps … 400.00 µs) 300.00 µs █▁▁▁▁▁▁█▁▁▁▁▁▄▁▁▁▁▁▁▃ +unicode-rs/unicode-segmentation (wasm-bindgen) 21.52 µs/iter 0.00 ps █ + (0.00 ps … 300.00 µs) 100.00 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅ +Intl.Segmenter 5.24 µs/iter 5.22 µs ▂█▇ + (5.05 µs … 6.01 µs) 5.96 µs ▂███▆▂▂▂▁▁▂▁▁▁▁▁▁▁▁▃▂ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.60 µs + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 65.62 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 69.30 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 74.14 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■ 21.52 µs + Intl.Segmenter ┤■ 5.24 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.01x faster than Intl.Segmenter + 8.27x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 25.22x faster than graphemer + 26.63x faster than grapheme-splitter + 28.5x faster than @formatjs/intl-segmenter + +• Emojis +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 829.68 ns/iter 878.91 ns █ ▄ + (732.42 ns … 952.15 ns) 927.73 ns ▁▁█▃▁█▁▃▃▁▁▁▆▁▁█▁▅▄▁▄ +graphemer 6.61 µs/iter 6.64 µs █ ▄ █ ▄ ▆ + (6.52 µs … 6.79 µs) 6.74 µs ▃▁█▁█▁▁█▁█▁█▁▁▁▁▅▁▅▁▇ +grapheme-splitter 19.62 µs/iter 0.00 ps █ + (0.00 ps … 300.00 µs) 100.00 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄ +@formatjs/intl-segmenter 11.55 µs/iter 0.00 ps █ + (0.00 ps … 300.00 µs) 100.00 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃ +unicode-rs/unicode-segmentation (wasm-bindgen) 4.66 µs/iter 4.71 µs █▅ ▃ + (4.49 µs … 4.93 µs) 4.86 µs ▂▂▁█▄█▁██▆▁██▂▁▄▅▂▁▄▂ +Intl.Segmenter 2.34 µs/iter 2.34 µs █ ▅ + (2.27 µs … 2.54 µs) 2.44 µs ▃▁▁█▁▁█▁▁█▁▄▁▁▃▁▁▃▁▁▂ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 829.68 ns + graphemer ┤■■■■■■■■■■ 6.61 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 19.62 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■ 11.55 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■ 4.66 µs + Intl.Segmenter ┤■■■ 2.34 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.82x faster than Intl.Segmenter + 5.62x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 7.97x faster than graphemer + 13.92x faster than @formatjs/intl-segmenter + 23.64x faster than grapheme-splitter + +• Hindi +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.43 µs/iter 2.47 µs ▃ ▃ ▆ █ + (2.34 µs … 2.59 µs) 2.59 µs ▆▁█▁█▁█▁█▁█▁▅▁▃▁▃▁▅▁▂ +graphemer 24.71 µs/iter 24.68 µs ▃ █ ▃ + (24.63 µs … 25.02 µs) 24.73 µs █▁▁▁▁█▁▁▁▁█▁▁▁▁▁▁▁▁▁▆ +grapheme-splitter 67.34 µs/iter 100.00 µs ▇ █ + (0.00 ps … 500.00 µs) 300.00 µs █▁▁▁▁▁▁█▁▁▁▁▁▂▁▁▁▁▁▁▂ +@formatjs/intl-segmenter 30.93 µs/iter 30.91 µs █ ██ + (30.69 µs … 31.57 µs) 31.32 µs █▁████▁█▁▁▁▁▁▁▁█▁▁▁▁█ +unicode-rs/unicode-segmentation (wasm-bindgen) 32.40 µs/iter 100.00 µs █ + (0.00 ps … 500.00 µs) 300.00 µs █▁▁▁▁▁▁▆▁▁▁▁▁▁▁▁▁▁▁▁▁ +Intl.Segmenter 5.23 µs/iter 5.25 µs █▇▂ + (5.13 µs … 5.66 µs) 5.59 µs ▂▆███▇▄▁▅▁▁▁▁▁▁▁▁▁▁▂▂ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.43 µs + graphemer ┤■■■■■■■■■■■■ 24.71 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 67.34 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■ 30.93 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■■■■■■■■ 32.40 µs + Intl.Segmenter ┤■ 5.23 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.15x faster than Intl.Segmenter + 10.15x faster than graphemer + 12.71x faster than @formatjs/intl-segmenter + 13.31x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 27.67x faster than grapheme-splitter + +• Demonic characters +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 476.73 ns/iter 537.11 ns ▅▂ ▆ ▃ █ + (390.62 ns … 610.35 ns) 585.94 ns ▅▁██▁█▁▁▂▁▃▁█▃▁█▁▄▅▁▂ +graphemer 13.23 µs/iter 13.31 µs ▄ █ ▄ ▄ + (13.13 µs … 13.38 µs) 13.33 µs █▁█▁▁█▁█▁▁▅▁▅▁▁▁▁█▁▁█ +grapheme-splitter 10.31 µs/iter 10.42 µs █ █ █ ▃▃▃ ▃ + (10.03 µs … 10.67 µs) 10.64 µs ▆█▆█▁▁█▁███▁█▆▆▆▆▁▆▆▆ +@formatjs/intl-segmenter 11.79 µs/iter 11.84 µs ▄ █ █ + (11.67 µs … 12.01 µs) 11.96 µs █▁█▅▁█▁█▅▁█▁█▅▁▅▁▁▁▁▅ +unicode-rs/unicode-segmentation (wasm-bindgen) 2.38 µs/iter 2.42 µs ▄▄ █ ▇ ▂ + (2.27 µs … 2.61 µs) 2.54 µs ▅▁▇▁██▁█▁█▁█▁█▁▆▅▁▁▁▂ +Intl.Segmenter 1.30 µs/iter 1.32 µs █ ▄ ▃ + (1.25 µs … 1.44 µs) 1.39 µs █▁▁█▁▁▁█▁▁█▁▁▇▁▁▁▃▁▁▄ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 476.73 ns + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 13.23 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■ 10.31 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 11.79 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 2.38 µs + Intl.Segmenter ┤■■ 1.30 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.73x faster than Intl.Segmenter + 4.99x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 21.63x faster than grapheme-splitter + 24.74x faster than @formatjs/intl-segmenter + 27.76x faster than graphemer + +• Tweet text (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 3.80 µs/iter 3.81 µs █ ▃ + (3.76 µs … 3.96 µs) 3.91 µs █▁▁█▁▁▁█▁▁▄▁▁▂▁▁▁▃▁▁▃ +graphemer 89.59 µs/iter 100.00 µs █ ▆ + (0.00 ps … 500.00 µs) 400.00 µs █▁▁▁▁█▁▁▁▁▃▁▁▁▁▄▁▁▁▁▁ +grapheme-splitter 119.21 µs/iter 200.00 µs █ + (0.00 ps … 400.00 µs) 200.00 µs ▃▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ +@formatjs/intl-segmenter 47.43 µs/iter 47.49 µs █ ▃ + (47.00 µs … 48.32 µs) 47.97 µs ▆▁▁▆█▁▆▁▆▆█▁▁▁▁▁▁▁▁▁▆ +unicode-rs/unicode-segmentation (wasm-bindgen) 21.39 µs/iter 21.39 µs █ █ ▃ + (21.34 µs … 21.53 µs) 21.48 µs █▁▁█▁▁▁█▁▁▁▁▁▆▁▁▁▁▁▁▆ +Intl.Segmenter 9.15 µs/iter 9.18 µs █ ▅ ▅ ▅ ▅ ▂ + (9.06 µs … 9.28 µs) 9.28 µs ▇▁█▁█▁▁█▁█▁█▁▇▁▁▄▁█▁▄ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 3.80 µs + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■■■■ 89.59 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 119.21 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■ 47.43 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 21.39 µs + Intl.Segmenter ┤■■ 9.15 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.41x faster than Intl.Segmenter + 5.63x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 12.48x faster than @formatjs/intl-segmenter + 23.56x faster than graphemer + 31.36x faster than grapheme-splitter + +• Code snippet (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 9.30 µs/iter 9.30 µs ▂ ▆ █ + (9.20 µs … 9.52 µs) 9.42 µs ▇▁▅▁█▁▁█▁█▁▃▁▅▁▁▅▁▃▁▃ +graphemer 201.78 µs/iter 300.00 µs █ + (0.00 ps … 500.00 µs) 500.00 µs ▄▁▁▁█▁▁▁▇▁▁▁▇▁▁▁█▁▁▁▁ +grapheme-splitter 307.69 µs/iter 400.00 µs ▃ █ + (100.00 µs … 600.00 µs) 600.00 µs ▂▁▁▁█▁▁▁█▁▁▁▅▁▁▁▅▁▁▁▁ +@formatjs/intl-segmenter 156.04 µs/iter 200.00 µs █ ▂ + (0.00 ps … 600.00 µs) 500.00 µs ▃▁▁▁█▁▁▁█▁▁▁▂▁▁▁▃▁▁▁▁ +unicode-rs/unicode-segmentation (wasm-bindgen) 49.54 µs/iter 49.76 µs █ █ + (48.85 µs … 50.02 µs) 49.85 µs █▁▁▁▁▁▁▁█▁▁██▁██▁▁███ +Intl.Segmenter 18.21 µs/iter 18.21 µs █ █ + (18.14 µs … 18.43 µs) 18.33 µs █▁▅▁▁▅▁▁█▁█▁▁▁▁▁▁▁▁▁▅ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 9.30 µs + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■ 201.78 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 307.69 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■ 156.04 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 49.54 µs + Intl.Segmenter ┤■ 18.21 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.96x faster than Intl.Segmenter + 5.33x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 16.78x faster than @formatjs/intl-segmenter + 21.7x faster than graphemer + 33.09x faster than grapheme-splitter diff --git a/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-firefox_154.0b1.txt b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-firefox_154.0b1.txt new file mode 100644 index 0000000..4f22ac1 --- /dev/null +++ b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-firefox_154.0b1.txt @@ -0,0 +1,197 @@ +clk: ~3.90 GHz +cpu: null +runtime: null (null) + +benchmark avg (min … max) p75 / p99 (min … top 1%) +------------------------------------------------------------- ------------------------------- +• Lorem ipsum (ascii) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 5.53 µs/iter 5.62 µs █ ▇ + (5.37 µs … 5.86 µs) 5.86 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▄ +graphemer 19.95 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +grapheme-splitter 40.53 µs/iter 0.00 ps █ + (0.00 ps … 2.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +@formatjs/intl-segmenter 32.45 µs/iter 0.00 ps █ + (0.00 ps … 5.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +unicode-rs/unicode-segmentation (wasm-bindgen) 14.96 µs/iter 15.14 µs ▃ █ ▆ + (14.65 µs … 15.38 µs) 15.38 µs █▁▁▁▁▁▁█▁▁▁▁▁█▁▁▁▁▁▁▄ +Intl.Segmenter 3.11 µs/iter 3.17 µs ▃ █ + (2.93 µs … 3.91 µs) 3.42 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▃ + + ┌ ┐ + unicode-segmenter/grapheme ┤■■ 5.53 µs + graphemer ┤■■■■■■■■■■■■■■■ 19.95 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 40.53 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■ 32.45 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■■■ 14.96 µs + Intl.Segmenter ┤ 3.11 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.78x slower than Intl.Segmenter + 2.7x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 3.61x faster than graphemer + 5.87x faster than @formatjs/intl-segmenter + 7.33x faster than grapheme-splitter + +• Emojis +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 1.61 µs/iter 1.71 µs ▆ █ + (1.46 µs … 1.95 µs) 1.95 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▁ +graphemer 5.77 µs/iter 5.86 µs ▂ █ + (5.62 µs … 6.10 µs) 5.86 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +grapheme-splitter 24.70 µs/iter 0.00 ps █ + (0.00 ps … 2.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +@formatjs/intl-segmenter 9.63 µs/iter 9.77 µs █ ▄ + (9.52 µs … 9.77 µs) 9.77 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +unicode-rs/unicode-segmentation (wasm-bindgen) 4.62 µs/iter 4.64 µs █ + (4.39 µs … 4.88 µs) 4.88 µs ▄▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▂ +Intl.Segmenter 1.08 µs/iter 1.22 µs █ + (732.42 ns … 4.39 µs) 1.22 µs ▂▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ + + ┌ ┐ + unicode-segmenter/grapheme ┤■ 1.61 µs + graphemer ┤■■■■■■■ 5.77 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 24.70 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■ 9.63 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 4.62 µs + Intl.Segmenter ┤ 1.08 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.49x slower than Intl.Segmenter + 2.88x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 3.6x faster than graphemer + 6x faster than @formatjs/intl-segmenter + 15.38x faster than grapheme-splitter + +• Hindi +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 5.24 µs/iter 5.37 µs █ ▆ + (5.13 µs … 5.37 µs) 5.37 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +graphemer 19.80 µs/iter 19.78 µs █ + (19.53 µs … 20.02 µs) 20.02 µs ▅▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▅ +grapheme-splitter 52.96 µs/iter 54.20 µs █ █ █ + (47.36 µs … 55.91 µs) 54.69 µs █▁▁▁▁▁▁▁▁▁▁▁███▁██▁██ +@formatjs/intl-segmenter 32.39 µs/iter 32.47 µs ▂ █ + (31.98 µs … 33.20 µs) 32.71 µs █▁▁▁▁▁▁▇▁▁▁▁▁█▁▁▁▁▁▁▄ +unicode-rs/unicode-segmentation (wasm-bindgen) 14.73 µs/iter 14.89 µs █ ▂ + (14.40 µs … 14.89 µs) 14.89 µs ▃▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ +Intl.Segmenter 2.65 µs/iter 2.69 µs █ + (2.44 µs … 2.93 µs) 2.93 µs ▄▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▂ + + ┌ ┐ + unicode-segmenter/grapheme ┤■■ 5.24 µs + graphemer ┤■■■■■■■■■■■■ 19.80 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 52.96 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■ 32.39 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■ 14.73 µs + Intl.Segmenter ┤ 2.65 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.97x slower than Intl.Segmenter + 2.81x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 3.78x faster than graphemer + 6.18x faster than @formatjs/intl-segmenter + 10.11x faster than grapheme-splitter + +• Demonic characters +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 858.56 ns/iter 976.56 ns ▅ █ + (488.28 ns … 1.22 µs) 1.22 µs ▂▁▁▁▁▁▁█▁▁▁▁▁█▁▁▁▁▁▁▁ +graphemer 9.38 µs/iter 9.52 µs █ + (9.28 µs … 9.77 µs) 9.52 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +grapheme-splitter 8.27 µs/iter 8.30 µs █ + (8.06 µs … 8.54 µs) 8.54 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▄ +@formatjs/intl-segmenter 11.91 µs/iter 11.96 µs ▃ █ + (11.72 µs … 12.21 µs) 12.21 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▄ +unicode-rs/unicode-segmentation (wasm-bindgen) 2.53 µs/iter 2.69 µs █ ▄ + (2.20 µs … 2.69 µs) 2.69 µs ▃▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ +Intl.Segmenter 446.94 ns/iter 488.28 ns ▃ █ + (0.00 ps … 4.15 µs) 3.91 µs ▁█▁█▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ┌ ┐ + unicode-segmenter/grapheme ┤■ 858.56 ns + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■■■■■ 9.38 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■ 8.27 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 11.91 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■ 2.53 µs + Intl.Segmenter ┤ 446.94 ns + └ ┘ + +summary + unicode-segmenter/grapheme + 1.92x slower than Intl.Segmenter + 2.94x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 9.63x faster than grapheme-splitter + 10.92x faster than graphemer + 13.87x faster than @formatjs/intl-segmenter + +• Tweet text (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 8.11 µs/iter 8.06 µs █ + (8.06 µs … 8.30 µs) 8.30 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅ +graphemer 29.74 µs/iter 29.79 µs ▄ █ + (29.30 µs … 30.52 µs) 30.03 µs █▁▁▁▁▁▁█▁▁▁▁▁█▁▁▁▁▁▁█ +grapheme-splitter 100.08 µs/iter 103.76 µs █ + (93.75 µs … 109.13 µs) 108.89 µs ██▁███▁██▁▁▁▁█▁▁█▁▁▁█ +@formatjs/intl-segmenter 44.29 µs/iter 44.68 µs █ █ █ █ + (43.21 µs … 45.17 µs) 45.17 µs █▁▁▁▁▁▁▁█▁█▁▁█▁█▁▁█▁█ +unicode-rs/unicode-segmentation (wasm-bindgen) 22.71 µs/iter 22.71 µs █ █ ▃ + (22.22 µs … 23.44 µs) 22.95 µs ▆▁▁▁▁▁▁█▁▁▁▁▁█▁▁▁▁▁▁█ +Intl.Segmenter 4.52 µs/iter 4.64 µs █ █ + (4.39 µs … 4.64 µs) 4.64 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ + + ┌ ┐ + unicode-segmenter/grapheme ┤■ 8.11 µs + graphemer ┤■■■■■■■■■ 29.74 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 100.08 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■ 44.29 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■ 22.71 µs + Intl.Segmenter ┤ 4.52 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.8x slower than Intl.Segmenter + 2.8x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 3.67x faster than graphemer + 5.46x faster than @formatjs/intl-segmenter + 12.34x faster than grapheme-splitter + +• Code snippet (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 19.36 µs/iter 19.53 µs █ █ + (19.04 µs … 19.53 µs) 19.53 µs ▇▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ +graphemer 68.83 µs/iter 68.85 µs █ + (68.12 µs … 70.07 µs) 69.34 µs ▅▁▁▁█▁▁▁█▁▁▁█▁▁▁▅▁▁▁▅ +grapheme-splitter 218.20 µs/iter 220.95 µs █ + (213.13 µs … 224.85 µs) 223.14 µs █▁▁██▁█▁█▁██▁▁▁▁██▁▁█ +@formatjs/intl-segmenter 102.17 µs/iter 103.03 µs █ █ █ + (100.59 µs … 103.52 µs) 103.27 µs █▁▁▁▁█▁█▁█▁▁▁█▁▁█▁█▁█ +unicode-rs/unicode-segmentation (wasm-bindgen) 53.30 µs/iter 0.00 ps █ + (0.00 ps … 2.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂ +Intl.Segmenter 10.35 µs/iter 10.50 µs █ + (10.25 µs … 10.74 µs) 10.50 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ + + ┌ ┐ + unicode-segmenter/grapheme ┤■ 19.36 µs + graphemer ┤■■■■■■■■■■ 68.83 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 218.20 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■ 102.17 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■ 53.30 µs + Intl.Segmenter ┤ 10.35 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.87x slower than Intl.Segmenter + 2.75x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 3.55x faster than graphemer + 5.28x faster than @formatjs/intl-segmenter + 11.27x faster than grapheme-splitter diff --git a/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-nodejs_26.5.0.txt b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-nodejs_26.5.0.txt new file mode 100644 index 0000000..f5768df --- /dev/null +++ b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-nodejs_26.5.0.txt @@ -0,0 +1,265 @@ +clk: ~4.43 GHz +cpu: Apple M4 Pro +runtime: node 26.5.0 (arm64-darwin) + +benchmark avg (min … max) p75 / p99 (min … top 1%) +------------------------------------------------------------- ------------------------------- +• Lorem ipsum (ascii) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.65 µs/iter 2.67 µs ▂ ▃██ ▃ + (2.56 µs … 2.86 µs) 2.83 µs ▅▅█████▂█▇▆▃▄▄▄▂▂▁▁▁▂ + gc(717.38 µs … 1.58 ms) 881.87 b (864.51 b…954.84 b) + +graphemer 41.61 µs/iter 41.63 µs █ + (31.75 µs … 97.83 µs) 55.88 µs ▁▁▁▁▂▁▅█▇▃▂▂▂▂▁▁▁▁▁▁▁ + gc(778.67 µs … 1.60 ms) 81.71 kb ( 16.27 kb…444.19 kb) + +grapheme-splitter 69.07 µs/iter 57.25 µs █ + (48.50 µs … 659.21 µs) 605.88 µs █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + gc(772.00 µs … 1.72 ms) 83.12 kb ( 26.08 kb… 1.96 mb) + +@formatjs/intl-segmenter 32.22 µs/iter 32.32 µs █ + (31.84 µs … 32.66 µs) 32.53 µs █▁█▁▁███▁▁▁██▁█▁▁▁█▁█ + gc(866.88 µs … 1.49 ms) 2.80 kb ( 2.63 kb… 4.71 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 20.63 µs/iter 21.42 µs ▅█ ▂ + (15.79 µs … 75.83 µs) 37.46 µs ▁▅████▅▃▃▂▁▂▁▁▁▁▁▁▁▁▁ + gc(720.33 µs … 1.59 ms) 22.42 kb (888.00 b… 61.86 kb) + +Intl.Segmenter 5.30 µs/iter 5.32 µs ▃▆█▅ ▃ + (5.22 µs … 5.59 µs) 5.59 µs ▆███████▁▁▃▃▃▁▁▁▁▁▁▁▃ + gc( 2.36 ms … 4.01 ms) 5.92 kb ( 5.90 kb… 5.93 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.65 µs + graphemer ┤■■■■■■■■■■■■■■■■■■■■ 41.61 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 69.07 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■ 32.22 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■ 20.63 µs + Intl.Segmenter ┤■ 5.30 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2x faster than Intl.Segmenter + 7.79x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 12.17x faster than @formatjs/intl-segmenter + 15.71x faster than graphemer + 26.08x faster than grapheme-splitter + +• Emojis +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 829.70 ns/iter 839.14 ns ▇█▆▃▂▂ + (789.99 ns … 1.13 µs) 910.24 ns ▃▆███████▆▅▄▃▂▂▃▃▂▁▁▁ + gc(722.21 µs … 1.74 ms) 5.27 kb ( 5.24 kb… 5.28 kb) + +graphemer 7.60 µs/iter 7.63 µs ▃ █ + (7.49 µs … 7.77 µs) 7.76 µs ▄█▄█▆▄██▆██▄▄█▄▄▁▁▄▁▄ + gc(837.54 µs … 1.43 ms) 7.66 kb ( 7.61 kb… 7.68 kb) + +grapheme-splitter 43.18 µs/iter 28.46 µs █ + (22.04 µs … 320.38 µs) 287.88 µs █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▁ + gc(757.63 µs … 1.62 ms) 42.26 kb ( 12.53 kb… 1.55 mb) + +@formatjs/intl-segmenter 10.19 µs/iter 10.30 µs █ + (9.99 µs … 10.62 µs) 10.50 µs ▅▅▃▃█▃▅▅▅▁▁▁▅▁▁▃▇▃▁▁▃ + gc(872.88 µs … 1.45 ms) 4.08 kb ( 4.05 kb… 4.08 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 4.39 µs/iter 4.43 µs ██▆▆█▆▆ ▃ + (4.22 µs … 4.54 µs) 4.53 µs ▄▁▄▆██████████▄▆███▆▄ + gc(761.42 µs … 1.32 ms) 4.93 kb ( 4.77 kb… 5.39 kb) + +Intl.Segmenter 2.24 µs/iter 2.25 µs ▂█ ▄ + (2.19 µs … 2.33 µs) 2.31 µs ▇▄▆▅███▆█▇▇█▅▆▃▁▄▆▂▃▂ + gc( 2.24 ms … 3.52 ms) 4.45 kb ( 4.44 kb… 4.45 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 829.70 ns + graphemer ┤■■■■■ 7.60 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 43.18 µs + @formatjs/intl-segmenter ┤■■■■■■■■ 10.19 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■ 4.39 µs + Intl.Segmenter ┤■ 2.24 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.7x faster than Intl.Segmenter + 5.29x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 9.17x faster than graphemer + 12.28x faster than @formatjs/intl-segmenter + 52.05x faster than grapheme-splitter + +• Hindi +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.75 µs/iter 2.79 µs ▅▅▆▃▂█ + (2.60 µs … 2.87 µs) 2.83 µs ▄▂█▂▆▂▃▂▂▃▂███████▃█▆ + gc(780.29 µs … 1.40 ms) 6.26 kb ( 6.25 kb… 6.27 kb) + +graphemer 28.26 µs/iter 28.41 µs █ ██ █ ██ █ █ + (28.06 µs … 28.47 µs) 28.45 µs █▁▁▁██▁█▁██▁▁▁▁▁▁▁█▁█ + gc(883.04 µs … 1.42 ms) 7.56 kb ( 7.55 kb… 7.58 kb) + +grapheme-splitter 59.74 µs/iter 60.96 µs ▂▃█▃ + (53.42 µs … 92.92 µs) 78.67 µs ▂▃██████▃▂▂▂▁▁▁▁▁▁▁▁▁ + gc(782.21 µs … 1.34 ms) 50.99 kb ( 6.87 kb…136.87 kb) + +@formatjs/intl-segmenter 30.90 µs/iter 30.99 µs █ █ █ + (30.61 µs … 31.53 µs) 31.06 µs █▁▁▁█▁▁▁███▁█▁▁▁▁█▁▁█ + gc(884.67 µs … 1.25 ms) 7.40 kb ( 7.39 kb… 7.42 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 13.21 µs/iter 13.24 µs █ + (13.08 µs … 13.43 µs) 13.43 µs ▅█▅▅█▅█▅██▁▅▁▅▅▁▁▁▁▁▅ + gc(845.25 µs … 1.41 ms) 5.61 kb ( 5.59 kb… 5.61 kb) + +Intl.Segmenter 5.44 µs/iter 5.47 µs █▄▄█ ▂ + (5.29 µs … 5.63 µs) 5.61 µs ▃▁▅▃▁▃▇████▇█▁▇▃▁▃▁▁▃ + gc( 2.31 ms … 2.97 ms) 3.47 kb ( 3.45 kb… 3.47 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.75 µs + graphemer ┤■■■■■■■■■■■■■■■ 28.26 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 59.74 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■ 30.90 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■ 13.21 µs + Intl.Segmenter ┤■■ 5.44 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.98x faster than Intl.Segmenter + 4.81x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 10.29x faster than graphemer + 11.25x faster than @formatjs/intl-segmenter + 21.75x faster than grapheme-splitter + +• Demonic characters +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 457.21 ns/iter 460.80 ns ▃▆█▆▆ + (431.57 ns … 491.13 ns) 479.99 ns ▂▁▂▂▂▄▅▅██████▄▃▂▄▃▃▂ + gc(694.38 µs … 1.34 ms) 1.34 kb ( 1.32 kb… 1.35 kb) + +graphemer 15.15 µs/iter 15.19 µs ███ █ █ + (15.00 µs … 15.38 µs) 15.30 µs █▁█▁███████▁█▁█▁█▁▁▁█ + gc(862.58 µs … 1.28 ms) 4.79 kb ( 4.75 kb… 4.88 kb) + +grapheme-splitter 11.08 µs/iter 11.18 µs █ + (10.81 µs … 11.38 µs) 11.33 µs █▁▅▁▅▅█▅▅▅█▅█▅▅▁█▅▅▁█ + gc(839.92 µs … 1.47 ms) 3.21 kb ( 3.17 kb… 3.25 kb) + +@formatjs/intl-segmenter 11.87 µs/iter 11.91 µs ▃ ▃ ▃▃▃▃ █ + (11.75 µs … 12.09 µs) 11.97 µs ▆▁▆█▁▆█▁▆████▆█▆▁▁▆▁▆ + gc(840.25 µs … 1.45 ms) 2.56 kb ( 2.54 kb… 2.57 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 2.20 µs/iter 2.23 µs ▃ ▆▂█▂▆ ▇ + (2.14 µs … 2.32 µs) 2.31 µs ▃█▅██████▆█▆▅▅▄▂▃▁▂▄▂ + gc(708.71 µs … 1.69 ms) 1.28 kb ( 1.06 kb… 1.38 kb) + +Intl.Segmenter 1.05 µs/iter 1.07 µs ▇▄██▅▅▂▆ + (978.07 ns … 1.18 µs) 1.17 µs ▂▆██████████▆█▄▄▅▃▃▄▂ + gc( 2.15 ms … 3.04 ms) 1.23 kb ( 1.20 kb… 1.23 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 457.21 ns + graphemer ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 15.15 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■ 11.08 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■ 11.87 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■ 2.20 µs + Intl.Segmenter ┤■ 1.05 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.3x faster than Intl.Segmenter + 4.82x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 24.24x faster than grapheme-splitter + 25.95x faster than @formatjs/intl-segmenter + 33.14x faster than graphemer + +• Tweet text (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 4.12 µs/iter 4.15 µs ▃ ▃ █ + (4.03 µs … 4.22 µs) 4.22 µs ▃▃▆▄▆██████████▃▃▃▄▄▃ + gc(810.04 µs … 1.25 ms) 1.34 kb ( 1.32 kb… 1.34 kb) + +graphemer 54.78 µs/iter 55.71 µs █ + (44.71 µs … 82.83 µs) 71.54 µs ▁▁▁▁▅▇█▇█▄▃▂▂▂▁▂▂▂▁▁▁ + gc(741.75 µs … 1.49 ms) 114.20 kb ( 77.96 kb…241.96 kb) + +grapheme-splitter 108.15 µs/iter 109.71 µs █▃ + (94.08 µs … 137.71 µs) 127.21 µs ▁▁▁▁▁▂████▅▄▂▂▂▁▁▁▁▁▁ + gc(753.21 µs … 1.30 ms) 66.87 kb ( 63.05 kb…151.20 kb) + +@formatjs/intl-segmenter 47.46 µs/iter 47.56 µs █ ▃ ▃ + (47.12 µs … 47.84 µs) 47.70 µs ▆▁▁▁▁▁▆▁█▁▁█▁▁▆█▁▁▁▁▆ + gc(879.63 µs … 1.35 ms) 244.74 b (221.84 b…270.12 b) + +unicode-rs/unicode-segmentation (wasm-bindgen) 18.90 µs/iter 18.98 µs █ █ █ + (18.68 µs … 19.06 µs) 19.04 µs █▁▁██▁▁▁▁██▁█▁▁▁██▁██ + gc(854.42 µs … 1.28 ms) 162.16 b (145.73 b…168.67 b) + +Intl.Segmenter 8.53 µs/iter 8.55 µs █ ▂ ▂▂▂ ▂ + (8.41 µs … 8.78 µs) 8.74 µs ▇▁█▇█▇███▇█▇▁▁▁▄▄▁▁▁▄ + gc( 1.83 ms … 2.50 ms) 4.19 kb ( 4.18 kb… 4.19 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 4.12 µs + graphemer ┤■■■■■■■■■■■■■■■■■ 54.78 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 108.15 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■ 47.46 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 18.90 µs + Intl.Segmenter ┤■ 8.53 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.07x faster than Intl.Segmenter + 4.59x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 11.52x faster than @formatjs/intl-segmenter + 13.29x faster than graphemer + 26.24x faster than grapheme-splitter + +• Code snippet (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 9.99 µs/iter 10.05 µs ▃▃ █ + (9.67 µs … 10.19 µs) 10.16 µs ▆▁▁▁▁▁▁▁▁▄▄▆██▆█▆█▁▁▄ + gc(860.33 µs … 1.34 ms) 5.35 kb ( 5.33 kb… 5.35 kb) + +graphemer 116.51 µs/iter 117.46 µs █▇ + (108.63 µs … 153.75 µs) 142.08 µs ▁▂▆███▇▃▂▁▁▁▁▁▁▁▁▁▁▁▁ + gc(734.21 µs … 1.71 ms) 270.95 kb (270.40 kb…526.40 kb) + +grapheme-splitter 237.59 µs/iter 239.38 µs ▄█▂ + (208.29 µs … 272.04 µs) 264.08 µs ▁▁▁▁▁▁▁▂▄███▆▃▂▂▂▁▁▁▁ + gc(682.38 µs … 1.07 ms) 157.19 kb ( 22.39 kb…412.31 kb) + +@formatjs/intl-segmenter 149.59 µs/iter 150.92 µs ▄█ + (134.54 µs … 177.67 µs) 168.92 µs ▁▁▁▁▁▂▅███▆▃▃▂▂▂▁▁▁▁▁ + gc(740.83 µs … 1.79 ms) 236.20 kb (112.45 kb…288.82 kb) + +unicode-rs/unicode-segmentation (wasm-bindgen) 44.52 µs/iter 45.12 µs ██ + (43.43 µs … 45.62 µs) 45.55 µs █▁█▁▁█▁██▁█▁▁▁▁▁█▁▁██ + gc(900.75 µs … 1.71 ms) 2.74 kb ( 2.73 kb… 2.74 kb) + +Intl.Segmenter 18.97 µs/iter 18.96 µs █ █ █ + (18.82 µs … 19.26 µs) 19.19 µs ████▁███▁▁▁▁█▁▁▁▁▁▁██ + gc( 1.14 ms … 3.39 ms) 1.12 kb ( 1.11 kb… 1.12 kb) + + ┌ ┐ + unicode-segmenter/grapheme ┤ 9.99 µs + graphemer ┤■■■■■■■■■■■■■■■■ 116.51 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 237.59 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■ 149.59 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■ 44.52 µs + Intl.Segmenter ┤■ 18.97 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 1.9x faster than Intl.Segmenter + 4.45x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 11.66x faster than graphemer + 14.97x faster than @formatjs/intl-segmenter + 23.77x faster than grapheme-splitter + +! = run with sudo to enable hardware counters diff --git a/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-safari_26.5.2.txt b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-safari_26.5.2.txt new file mode 100644 index 0000000..4bea65e --- /dev/null +++ b/benchmark/grapheme/_records/20260727-apple_m4_pro-macos_26.5-safari_26.5.2.txt @@ -0,0 +1,197 @@ +clk: ~4.41 GHz +cpu: null +runtime: null (null) + +benchmark avg (min … max) p75 / p99 (min … top 1%) +------------------------------------------------------------- ------------------------------- +• Lorem ipsum (ascii) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 1.72 µs/iter 1.71 µs █ + (1.46 µs … 2.44 µs) 2.20 µs ▂▁▁▁▁▁▁█▁▁▁▁▁▂▁▁▁▁▁▁▁ +graphemer 18.27 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +grapheme-splitter 72.73 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂ +@formatjs/intl-segmenter 37.71 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +unicode-rs/unicode-segmentation (wasm-bindgen) 18.20 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Intl.Segmenter 5.80 µs/iter 5.86 µs █ ▅ + (5.62 µs … 6.59 µs) 6.59 µs █▁▁▁▁█▁▁▁▁▁▁▁▁▁▃▁▁▁▁▂ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 1.72 µs + graphemer ┤■■■■■■■■ 18.27 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 72.73 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■ 37.71 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■ 18.20 µs + Intl.Segmenter ┤■■ 5.80 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 3.36x faster than Intl.Segmenter + 10.56x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 10.61x faster than graphemer + 21.89x faster than @formatjs/intl-segmenter + 42.21x faster than grapheme-splitter + +• Emojis +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 839.15 ns/iter 976.56 ns █ + (732.42 ns … 1.22 µs) 1.22 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▂ +graphemer 4.36 µs/iter 4.39 µs █ + (4.15 µs … 4.88 µs) 4.88 µs █▁▁▁▁▁▁█▁▁▁▁▁▃▁▁▁▁▁▁▁ +grapheme-splitter 16.60 µs/iter 16.60 µs █ + (16.36 µs … 17.33 µs) 17.09 µs █▁▁▁▁▁▁▇▁▁▁▁▁▃▁▁▁▁▁▁▅ +@formatjs/intl-segmenter 19.51 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +unicode-rs/unicode-segmentation (wasm-bindgen) 6.97 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 0.00 ps █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +Intl.Segmenter 4.67 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 0.00 ps █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 839.15 ns + graphemer ┤■■■■■■ 4.36 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 16.60 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 19.51 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■■■ 6.97 µs + Intl.Segmenter ┤■■■■■■■ 4.67 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 5.19x faster than graphemer + 5.57x faster than Intl.Segmenter + 8.3x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 19.78x faster than grapheme-splitter + 23.25x faster than @formatjs/intl-segmenter + +• Hindi +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 2.32 µs/iter 2.44 µs █ ▇ + (2.20 µs … 2.44 µs) 2.44 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +graphemer 15.87 µs/iter 15.63 µs █ + (15.38 µs … 18.07 µs) 17.82 µs █▁▆▁▁▁▃▁▁▁▃▁▁▁▁▁▁▁▁▁▃ +grapheme-splitter 32.37 µs/iter 32.71 µs █ ▄ + (31.74 µs … 33.45 µs) 33.20 µs █▁▁█▁▁▁▅▁▁▁▁▁█▁▁▁▁▁▁▅ +@formatjs/intl-segmenter 48.99 µs/iter 49.32 µs █ █ + (47.85 µs … 50.78 µs) 49.56 µs ▆▁▁▆▁▁▁▁▁█▁▆▁▁▆▁▁█▁▁▆ +unicode-rs/unicode-segmentation (wasm-bindgen) 13.81 µs/iter 13.92 µs █ ▄ + (13.67 µs … 14.16 µs) 14.16 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▃ +Intl.Segmenter 4.94 µs/iter 4.88 µs █ + (4.64 µs … 5.37 µs) 5.37 µs ▁▁▁▁▁▁▁█▁▁▁▁▁▅▁▁▁▁▁▁▁ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 2.32 µs + graphemer ┤■■■■■■■■■■ 15.87 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■ 32.37 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 48.99 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■ 13.81 µs + Intl.Segmenter ┤■■ 4.94 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.13x faster than Intl.Segmenter + 5.96x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 6.85x faster than graphemer + 13.97x faster than grapheme-splitter + 21.14x faster than @formatjs/intl-segmenter + +• Demonic characters +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 384.66 ns/iter 488.28 ns ▄ █ + (244.14 ns … 488.28 ns) 488.28 ns █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +graphemer 7.24 µs/iter 7.32 µs █ + (7.08 µs … 7.32 µs) 7.32 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█ +grapheme-splitter 6.46 µs/iter 6.59 µs █ ▂ + (6.35 µs … 6.84 µs) 6.84 µs █▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁▂ +@formatjs/intl-segmenter 60.14 µs/iter 60.79 µs ▃ █ ▃ + (59.08 µs … 61.52 µs) 61.28 µs █▁▁▁█▁▁▆▁▆▁▁▁▆▁▁█▁▁▁▆ +unicode-rs/unicode-segmentation (wasm-bindgen) 2.47 µs/iter 2.44 µs █ + (2.44 µs … 2.69 µs) 2.69 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃ +Intl.Segmenter 2.64 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 0.00 ps █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 384.66 ns + graphemer ┤■■■■ 7.24 µs + grapheme-splitter ┤■■■ 6.46 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 60.14 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■ 2.47 µs + Intl.Segmenter ┤■ 2.64 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 6.42x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 6.86x faster than Intl.Segmenter + 16.78x faster than grapheme-splitter + 18.83x faster than graphemer + 156.35x faster than @formatjs/intl-segmenter + +• Tweet text (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 4.25 µs/iter 4.15 µs ▃ █ + (3.91 µs … 8.06 µs) 6.84 µs █▁█▃▁▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +graphemer 27.24 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +grapheme-splitter 53.77 µs/iter 55.66 µs █ + (52.00 µs … 58.11 µs) 56.88 µs █▅▁█▅▁▁▁▁▁▁▁▁▁▁▅▅▁▁▁▅ +@formatjs/intl-segmenter 55.99 µs/iter 56.40 µs █ █ █ █ + (55.18 µs … 57.62 µs) 56.88 µs █▁▁█▁▁█▁▁█▁▁▁▁█▁▁█▁▁█ +unicode-rs/unicode-segmentation (wasm-bindgen) 20.86 µs/iter 21.00 µs █ + (20.51 µs … 21.48 µs) 21.00 µs ▄▁▁▁▁▁▁▁▁▁█▁▁▁▁▁▁▁▁▁█ +Intl.Segmenter 11.17 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1000.00 µs █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 4.25 µs + graphemer ┤■■■■■■■■■■■■■■■ 27.24 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 53.77 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 55.99 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■■■ 20.86 µs + Intl.Segmenter ┤■■■■■ 11.17 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2.63x faster than Intl.Segmenter + 4.91x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 6.41x faster than graphemer + 12.66x faster than grapheme-splitter + 13.18x faster than @formatjs/intl-segmenter + +• Code snippet (combined) +------------------------------------------------------------- ------------------------------- +unicode-segmenter/grapheme 9.80 µs/iter 9.77 µs █ + (9.52 µs … 10.50 µs) 10.25 µs █▁▁▁▁▁▁█▁▁▁▁▁▃▁▁▁▁▁▁▄ +graphemer 54.70 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂ +grapheme-splitter 125.31 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃ +@formatjs/intl-segmenter 124.37 µs/iter 0.00 ps █ + (0.00 ps … 1.00 ms) 1.00 ms █▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃ +unicode-rs/unicode-segmentation (wasm-bindgen) 50.35 µs/iter 50.05 µs ▃ █ ▃ + (48.58 µs … 57.86 µs) 51.51 µs █▁▆▁▁▁▁█▆▁█▁▆▁▁▁▁▁▁▁▆ +Intl.Segmenter 19.59 µs/iter 19.53 µs █ + (19.04 µs … 21.48 µs) 20.02 µs ▄▁▁▁▁█▁▁▁▁▆▁▁▁▁▄▁▁▁▁▄ + + ┌ ┐ + unicode-segmenter/grapheme ┤ 9.80 µs + graphemer ┤■■■■■■■■■■■■■ 54.70 µs + grapheme-splitter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 125.31 µs + @formatjs/intl-segmenter ┤■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 124.37 µs +unicode-rs/unicode-segmentation (wasm-bindgen) ┤■■■■■■■■■■■■ 50.35 µs + Intl.Segmenter ┤■■■ 19.59 µs + └ ┘ + +summary + unicode-segmenter/grapheme + 2x faster than Intl.Segmenter + 5.14x faster than unicode-rs/unicode-segmentation (wasm-bindgen) + 5.58x faster than graphemer + 12.69x faster than @formatjs/intl-segmenter + 12.79x faster than grapheme-splitter diff --git a/benchmark/grapheme/_records/report.generated.html b/benchmark/grapheme/_records/report.generated.html index d621022..fd5eea4 100644 --- a/benchmark/grapheme/_records/report.generated.html +++ b/benchmark/grapheme/_records/report.generated.html @@ -162,7 +162,7 @@

unicode-segmenter benchmark report

-

Grapheme segmentation, six workloads × six implementations. Longer bars are better (throughput). Generated 2026-07-07 from the records in this directory.

+

Grapheme segmentation, six workloads × six implementations. Longer bars are better (throughput). Generated 2026-07-27 from the records in this directory.

@@ -206,7 +206,7 @@

Performance history