5 ms·
Full Unicode Search at 50× ICU Speed with AVX‑512
- andersa 10mo agoFrom a German user perspective, ICU and your fancy library are incorrect, actually. Mass is not a different casing of Maß, they are different characters. Google likely changed this because it didn't do what users wanted.
- b2ccb2 10mo agoThe confusion likely stems from the relatively new introduction of the capitalized ẞ https://de.wikipedia.org/wiki/Gro%C3%9Fes_%C3%9F https://de.wikipedia.org/wiki/Gro%C3%9Fes_%C3%9F Maß capitalized (used to be) MASS. Funnily enough, Mass means one liter beer (think Oktoberfest).
- andersa 10mo agoIt's strange, because I would expect "maß" as the case insensitive search to match "MASS" in the search text, but "mass" should not match "Maß".
- janc_ 10mo agoI think all of those should be "tentative matches" for each other.
- looperhacks 10mo agoBoth Maß and Mass are valid spellings for a liter of beer ;) Not to confuse it with Maß, which just means any measurement, of course.
- looperhacks 10mo agoMASS is allowed casing of Maß, but not the preferred casing: https://www.rechtschreibrat.com/DOX/RfdR_Amtliches-Regelwerk_2024.pdf https://www.rechtschreibrat.com/DOX/RfdR_Amtliches-Regelwerk... Page 48
- pjmlp 10mo agoIt isn't until it is, how would you write it when ß isn't available on the keyboard? Which is why we also have to deal with the ue, ae, oe kind of trick, also known as Ersatzschreibweise. Then German language users from de-CH region, consider Mass the correct way. Yeah, localization and internalization is a mess to get right.
- wat10000 10mo agoCase insensitivity is localized like anything else. I and i are equivalent, right? Not if you’re doing Turkish, then it’s I and ı, and İ and i. In practice you can do pretty well with a universal approach, but it can’t be 100% correct.
- ashvardanian 10mo agoThis is a very good example! Still, “correct” needs context. You can be 100% “correct with respect to ICU”. It’s definitely not perfect, but it’s the best standard we have. And luckily for me, it also defines the locale-independent rules. I can expand to support locale-specific adjustments in the future, but waiting for the adoption to grow before investing even more engineering effort into this feature. Maybe worth opening a GitHub issue for that :)
- wat10000 10mo agoRight, nothing wrong with delegating the decision to a bunch of people who have thought long and hard about the best compromise, as long as it’s understood that it’s not perfect.
- Arnt 10mo agoAh, let's have a long discussion of this. Unicode avoids "different" and "same", https://www.unicode.org/reports/tr15/ https://www.unicode.org/reports/tr15/ uses phrases like compatibility equivalence. The whole thing is complicated, because it actually is complicated in the real world. You can spell the name of Gießen "Giessen" and most Germans consider it correct even if not ideal, but spelling Massachusetts "Maßachusetts" is plainly wrong in German text. The relationship between ß and ss isn't symmetric. Unicode captures that complexity, when you get into the fine details.
- mxmlnkn 10mo agoI never understood why the recommended replacement for ß is ss. It is a ligature of sz (similar to & being a ligature of et) and is even pronounced ess-zet. The only logical replacement would have been sz, and it would have avoided the clash of Masse (mass) and Maße (measurements). Then again, it only affects whether the vowel before it is pronounced short or long, and there are better ways to encode that in written language in the first place.
- adrian_b 10mo agoI agree that writing it "sz" might have created less problems. However, it is likely that it has never been pronounced "sz", but always "ss" and the habit of writing "sz" for the double consonant may have had the same reason as the writing of "ck" instead of the double "kk".
- mgaunard 10mo agoIn practice you should always normalize your Unicode data, then all you need to do is memcmp + boundary check. Interestingly enough this library doesn't provide grapheme cluster tokenization and/or boundary checking which is one of the most useful primitive for this.
- stingraycharles 10mo agoThat’s not practical in many situations, as the normalization alone may very well be more expensive than the search. If you’re in control of all data representations in your entire stack, then yes of course, but that’s hardly ever the case and different tradeoffs are made at different times (eg storage in UTF-8 because of efficiency, but in-memory representation in UTF-32 because of speed).
- mgaunard 10mo agoThat doesn't make sense; the search is doing on-the-fly normalization as part of its algorithm, so it cannot be faster than normalization alone.
- stingraycharles 10mo agoIt can, because of how CPUs work with registers and hot code paths and all that. First normalizing everything and then comparing normalized versions isn’t as fast. And it also enables “stopping early” when a match has been found / not found, you may not actually have to convert everything.
- mgaunard 10mo agoRunning more code per unit of data does not make the code hotter or reduce the register pressure, quite the opposite...
- stingraycharles 10mo agoYou’re misunderstanding: you just convert to 32 bits once and reuse that same register all the time. You’re running the exact same code, but are more more efficient in terms of “I immediately use the data for comparison after converting it”, which means it’s likely either in a register or L1 cache already.
- orthoxerox 10mo agoIs it possible to extend this to support additional transformation rules like Any-Latin;Latin-ASCII? To make it possible to find "Վարդանյան" in a haystack by searching for "vardanyan"?
- ashvardanian 10mo agoYes — fuzzy and phonetic matching across languages is part of the roadmap. That space is still poorly standardized, so I wanted to start with something widely understood and well-defined (ICU-style transforms) before layering on more advanced behavior. Also, as shown in the later tables, the Armenian and Georgian fast paths still have room for improvement. Before introducing higher-level APIs, I need to tighten the existing Armenian kernel and add a dedicated one for Georgian. It’s not a true bicameral script, but some characters are folding fold targets for older scripts, which currently forces too many fallbacks to the serial path.
- janc_ 10mo agoEven when transliteration is somewhat de-facto standardised, it usually is dependent on the target/host language. So e.g. Arabic & Russian are transliterated differently in e.g. English, French, German, Dutch, etc.
- unwind 10mo agoVery cool and impressive performance. I was worried (I find it confusing when Unicode "shadows" of normal letters exist, and those are of course also dangerous in some cases when they can be mis-interpreted for the letter they look more or less exactly like) by the article's use of U+212A (Kelvin symbol) as sample text, so I had to look it up [1]. Anyway, according to Wikipedia the dedicated symbol should not be used: However, this is a compatibility character provided for compatibility with legacy encodings. The Unicode standard recommends using U+004B K LATIN CAPITAL LETTER K instead; that is, a normal capital K. That was comforting, to me. :) [1]: https://en.wikipedia.org/wiki/Kelvin#Orthography https://en.wikipedia.org/wiki/Kelvin#Orthography
- jjmarr 10mo ago> I find it confusing when Unicode "shadows" of normal letters exist, and those are of course also dangerous in some cases when they can be mis-interpreted for the letter they look more or less exactly like Isn't this why Unicode normalization exists? This would let you compare Unicode letters and determine if they are canonically equivalent.
- ComputerGuru 10mo agoNormalization wouldn’t address this.
- happytoexplain 10mo agoWhat do you mean? All four normal forms of the Kelvin 'K' are the Latin 'K', as far as I can tell.
- nwellnhof 10mo agoNormalization forms NFKC and NFKD that also handle compatibility equivalence do.
- mananaysiempre 10mo agoA few deprecated characters, including the Kelvin and Ångström symbols, are in fact canonically equivalent to their replacements and not just compatibility equivalent, so plain NFC/NFD is enough. (It’s generally better to avoid NFKC/NFKD normalizations unless you fully understand the implications, as they do lose meaning and at the same time do not account for all possible confusables.)
- xking5205 10mo agoits good
- ashvardanian 10mo agoThis article is about the ugliest — but arguably the most important — piece of open-source software I’ve written this year. The write-up ended up long and dense, so here’s a short TL;DR: I grouped all Unicode 17 case-folding rules and built ~3K lines of AVX-512 kernels around them to enable fully standards-compliant, case-insensitive substring search across the entire 1M+ Unicode range, operating directly on UTF-8 bytes. In practice, this is often ~50× faster than ICU, and also less wrong than most tools people rely on today—from grep-style utilities to products like Google Docs, Microsoft Excel, and VS Code. StringZilla v4.5 is available for C99, C++11, Python 3, Rust, Swift, Go, and JavaScript. The article covers the algorithmic tradeoffs, benchmarks across 20+ Wikipedia dumps in different languages, and quick starts for each binding. Thanks to everyone for feature requests and bug reports. I'll do my best to port this to Arm as well — but first, I'm trying to ship one more thing before year's end.
- fatty_patty89 10mo agoThank you do the go bindings require cgo?
- ashvardanian 10mo agoThe GoLang bindings – yes, they are based on cGo. I realize it's suboptimal, but seems like the only practical option at this point.
- fatty_patty89 10mo agoIn a normal world the Go C FFI wouldn't have insane overhead but what can we do, the language is perfect and it will stay that way until morale improves. Thanks for the work you do
- kardianos 10mo agoIn a real (not "normal") world, trade-offs exist and Go choose a specific set of design points that are consequential.
- kardianos 10mo agoLooks neat. What are all the genomic sequence comparisons in there for? Is this a grab bag of interesting string methods or is there a motivation for this?
- ashvardanian 10mo agoLevenshtein distance calculations are a pretty generic string operation, Genomics happens to be one of the domains where they are most used... and a passion of mine :)
- moralestapia 10mo agoAsh is amazing! Also very cool and approachable guy. (Best wishes if you're reading this.)
- hans_castorp 10mo agoWould be cool if that could be integrated into Postgres :)
- ashvardanian 10mo agoI was just about to ask some friends about it. If I’m not mistaken, Postgres began using ICU for collation, but not string matching yet. Curious if someone here is working in that direction?
- bbminner 10mo agoI was really confused about the case folding, this page explained the motivation well https://jean.abou-samra.fr/blog/unicode-misconceptions https://jean.abou-samra.fr/blog/unicode-misconceptions """ Continuing with the previous example of “ß”, one has lowercase("ss") != lowercase("ß") but uppercase("ss") == uppercase("ß"). Conversely, for legacy reasons (compatibility with encodings predating Unicode), there exists a Kelvin sign “K”, which is distinct from the Latin uppercase letter “K”, but also lowercases to the normal Latin lowercase letter “k”, so that uppercase("K") != uppercase("K") but lowercase("K") == lowercase("K"). The correct way is to use Unicode case folding, a form of normalization designed specifically for case-insensitive comparisons. Both casefold("ß") == casefold("ss") and casefold("K") == casefold("K") are true. Case folding usually yields the same result as lowercasing, but not always (e.g., “ß” lowercases to itself but case-folds to “ss”). """ One question I have is why have Kelvin sign that is distinct from Latin K and other indistinguishable symbols? To make quantified machine readable (oh, this is not a 100K license plate or money amount, but a temperature)? Or to make it easier for specialized software to display it in correct placed/units?
- bee_rider 10mo agoThey seem to have (if I understand correctly) degree-Celsius and degree-Fahrenheit symbols. So maybe Kelvin is included for consistency, and it just happens to look identical to Latin K? IMO the confusing bit is giving it a lower case. It is a symbol that happens to look like an upper case, not an actual letter…
- bigwheels 10mo agoAnd why can't the symbol be a regular old uppercase "K"? Who is this helping?
- bee_rider 10mo agoI think just using uppercase Latin K is the recommendation. But, I dunno. Why would anybody apply upper or lower case operators to a temperature measurement? It just seems like a nonsense thing to do.
- _zagj 10mo ago> ICU has bindings for Rust that provide case-folding functionality, but not case-insensitive substring search. > ICU has many bindings. The Rust one doesn’t expose any substring search functionality, but the Python one does: Python's ICU support is based on ICU4C. Rust's ICU "bindings" are actually a new implementation called ICU4X, by developers who worked on i18n at Mozilla and Google and on ICU4C, with the goal of a cleaner, more performant implementation that is also memory safe. Maybe not relevant (as in substantially altering the benchmarks), but it's at least worth noting that the ICU backends aren't consistent throughout.
- ashvardanian 10mo agoThanks a lot for the correction! I'll adjust the references in a bit.
- bawolff 10mo agoWhile this is definitely really cool, wouldn't people who need this type of speed usually just store the text to be searched already case folded?
- user3939382 10mo agoI just used AVX-512 today for a lisp processor very performant.