Pretty nice. Always nice to see better optimized commonly required algorithms. If this is really faster than previous implementations (no reason to doubt, but don't have time to validate right now), this might save megawatts of power world wide. (Disclaimer: Would need to actually check with power measurement tools, but generally faster consumes less power.)
A small nitpick: not sure whether those macros bought anything, I guess the optimizer could inline function calls to intrinsic wrappers just fine as well?
That'd sure be neat! I really have no idea, though.
> A small nitpick: not sure whether those macros bought anything, I guess the optimizer could inline function calls to intrinsic wrappers just fine as well?
Yeah, that's a bit weird. It's my hacky generics-in-C method, that allows both AVX2 and SSE4 implementations to be compiled in one codebase, while keeping the base algorithm clean. The idea is that you can compile both versions by including multiple times, like so:
It looks like the main naive validator there (validate_utf8) clocks in at 1.25 cycles/byte for ASCII and 11 cycles/byte for UTF-8, which is respectively a 15.8x and 41.5x slowdown from my code.
Let's say I want to build a cross-platform app. I could use Electron (it's somewhat of a known quantity), or I could start doing some research into what else is available, and then evaluate the various offerings - not only to ensure they do what I need, but to also ensure that they use less power/resources than Electron. The effort here is (at least some kind of) unavoidable up-front cost/time.
To switch existing code to use some particular UTF-8 decoding library should be as simple as changing some imports, and perhaps some other (search/replace) code tweaks or implementing a quick shim/adapter. If it doesn't work out, rollback or abandon the branch. On most codebases, even larger ones, this whole process is probably an easier thing to do, requiring less effort than finding a decent (and more power efficient) alternative to Electron — seems to be the GP's gist. And personally I'd agree.
"Let's say I want to build a cross-platform app. I could use Electron (it's somewhat of a known quantity), or I could start doing some research into what else is available, and then evaluate the various offerings - not only to ensure they do what I need, but to also ensure that they use less power/resources than Electron. The effort here is (at least some kind of) unavoidable up-front cost/time."
Yup that's right. Any sign of inconvenience and efficiency be damned. Why do we even bother trying to fix environment. This cost money and inconveniences so many people.
Also it depends on your background. I have no problem doing without Electron. I would have to apply your exact logic to even look at it.
You are clearly someone seeing in black and white. There's shade of grays too, you know?
> Any sign of inconvenience and efficiency be damned.
Nope, not any signs... just that if you can build more with less time, in exchange of a bit of CPU efficiency, it's not so bad.
> Why do we even bother trying to fix environment.
For the future. You see, my program using a bit more RAM or CPU won't change much. Not eating that meat, that will change quite a bit, thus that's one of the things I do.
A software can allow people to be many orders in magnitudes more efficient, if I can build more of them, it allows even more people to be a few orders of magnitude more efficient. The power usage afterward, fit in the error margin quite well.
It's not quite that simple. A different algorithm and/or some well placed caching can easily outweigh the difference between a fast compiled (i.e. probably more efficient) language and slower interpreted one.
Obviously, a more efficient language with a better algorithm and good caching is best, but often we don't have the resources (whether it be time or money or whatever) to engineer the best solution, so we need to choose where to focus carefully. Sometimes that means faster prototyping in a language that may not be quite as efficient, and then using the extra time to iterate on algorithmic changes that may yield major benefits or put a caching layer in place.
Oh interesting, I hadn't seen that. It looks like it uses the same idea of shuffle-lookups on the first three nibbles. That's a fairly large patch though, I don't think I've fully grokked it. At the very least, my code differs from that in that mine does less byte stream shifting, instead doing shifts in the scalar domain. Their code also needs to deal with quotes, escaping, etc. due to being part of a JSON validator.
Just today I stumbled on lemire works when looking through the dependencies of jgit and found the java ewah bitset implemenation and ended up browsing his code seeing the utf8 validator and simdjson. Coincedences..
I have used the same "indendation" in the past. Because there are always review comments that his would be weird/confusing/unconventional I have given up on using it :(
It used to be the only valid way to indent macros. It has the advantage that macro lines always get highlighted in column-1 and stand out more from regular structured code.
Honestly? Give clang-format a try. There's tons of options available. Futz with them until you get a set that is to your liking, and then write them down in your repo's `.clang-format` file.
After that, the answer to "does this pull request comply with my formatting guidelines?" gets reduced to "does clang-format change this file?"
I think it's more common to put the indentation before the '#'. either way it ends up looking a little weird, but I prefer to see some visual indication of nesting.
(not op) Somewhat, although it's fairly minimal for avx2. The serious downclocking problem comes from avx512 instructions, which the author seems interested in:
> This algorithm should map fairly nicely to AVX-512, and should in fact be a bit faster than 2x the speed of AVX2 since a few instructions can be saved
In general, you aren't going to make a faster wheel without some trickery (vectorization usually). All the obvious optimizations have already been made in the stdlibs.
I can't remember where I read this, but there was an argument somewhere that worrying about downclocking only really makes sense when the instruction set in question is new. (Which AVX-512 certainly is, to be fair.) Eventually, when the instruction set is more widely used, you'll probably already be paying the downclocking penalty anyway because of someone else's code, and you might as well go ahead and use the fancy instructions in your own code. Do you think that's valid?
TL;DR; yes but not too much if your CPU is high-end and if the number of AVX instructions is low enough (and the slowdown is negligible in front of the gains if the number of instructions is high).
No, because non-"FMA unit" (mostly SIMD FP) AVX2 instructions don't cause license-based downclocking on recent Intel CPUs.
To get downclocking to L1 license (the middle speed tier), you need to use either FMA unit AVX/AVX2 insructions or any AVX-512 instructions. This algo doesn't use any of this, staying in the integer domain.
Of course this doesn't address say power or temperature based throttling which might be more likely to occur when wide SIMD is used, but it's not possible to give a precise answer there, other than many low or moderate core count setups won't see this kind of throttling outside of extreme use cases.
Thanks for the link, that's a good post! I hadn't investigated the throttling issues much in the past, not owning an AVX-512 machine, but that's much more precise than the standard "don't use AVX-512" meme that's repeated all over the place. Bookmarked!
I'm not sure if the compiler is smart enough to do it for you, but you might be able to squeak a bit more performance by precomputing a partial sum (data + V_LEN - 1) outside the loop for this statement:
> v_load(data + offset + V_LEN - 1);
Other than that, this is incredibly clean code and I doubt you can get much more performance without dropping into assembly. I wish my coworkers made code this well documented :/
At least my compiler (LLVM 10) is smart enough to know that "data" isn't modified, and thus fold the addresses of the two vector loads inside the main loop into single instructions:
I'd expect most modern compilers to get this right with optimizations on, but if somebody finds one that doesn't, I'd like to know. In general, I read through the disassembly a decent amount when developing this. That's how I noticed the "req += (vmask2_t)set << n;" (instead of |=) trick, which gets compiled to one lea instruction. The disassembly got a little bit hairy when I added the code to handle trailing bytes, though...
Thanks for the kind words, though! It warms my heart :)
> I'm not sure if the compiler is smart enough to do it for you, but you might be able to squeak a bit more performance by precomputing a partial sum (data + V_LEN - 1) outside the loop for this statement:
I don't remember last time when a compiler did not do this for me. Compilers are generally pretty smart nowadays.
The key to a solid claim, especially for something as bold as "fastest in the world", is a complete specification of the inputs to the benchmark.
You mention random ASCII bytes and "random UTF-8 bytes". The former is definitely an really important case but also the least interesting. I can write on a napkin a UTF-8-but-is-actually-ASCII decoder that approaches 256 bytes a cycle cached (with a fallback routine when the ASCII assumption fails).
So then what about the random UTF-8 bytes. What does it mean? Do you generate random bytes and then exclude invalid sequences? Do you generate a uniform random codepoint in the 21-bit codepoint space and then concert it to utf-8?
At a minimum it would good to see a benchmark with random distributions that approximate common languages.
The true fastest decoders on such realistic data will be adaptive and more complicated than yours.
Looking closer at it, I think that the distribution of random UTF-8 is not as uniform as it should be: it generates one byte first, and then generates continuation bytes depending on the value of that byte. Which means that half the code points will just be ASCII.
But I think this doesn't matter very much for benchmarking, at least for the mostly-branchless SIMD algorithms like mine. For each vector of input bytes, there's three branches that can get taken: the early exit for ASCII-only, and two branches that exit the loop due to validation failures, which don't really matter for benchmarking. Even though the distribution of code points will be roughly half ASCII, for a 32-byte AVX2 vector, the probability of a pure ASCII vector is something less than 2^-32 (since non-ASCII initial bytes translate into more than one byte of output, and the probability of 32 ASCII code points in a row is 2^-32, the probability of 32 bytes in a row is less, by an amount I'd rather not try to calculate). So for this particular benchmark, random UTF-8 should be roughly equivalent to "no ASCII". To verify, I disabled generating ASCII in the linked code, and the numbers came out pretty much identical. If anything, even more ASCII bytes would be a more interesting test, since that would make the quick-ASCII branch less predictable. If you know of any good UTF-8 corpora for common use cases, I'd be happy to benchmark them.
Given that different sets of "random UTF-8 bytes" generated with this method will virtually always follow the same path, the exact distribution doesn't really matter when measuring cycles/byte. Input that is purely 2-byte code points will be faster in terms of cycles/code point than purely 4-byte input, but that's mostly a property of UTF-8 being variable length.
I doubt there's much speed to be gained by adding any sort of adaptive behavior beyond the ASCII check. Generally these days you want as few branches as you can manage, and this algorithm has only one that matters.
> So for this particular benchmark, random UTF-8 should be roughly equivalent to "no ASCII". To verify, I disabled generating ASCII in the linked code, and the numbers came out pretty much identical. If anything, even more ASCII bytes would be a more interesting test, since that would make the quick-ASCII branch less predictable. If you know of any good UTF-8 corpora for common use cases, I'd be happy to benchmark them.
Indeed, this is a good example of a problem with random bechmarks: no text really has the behavior of uniform random with 50% ASCII but no character-to-character correlation. In the real world you often expect bursts of ASCII, e.g., where a title is written in ASCII or where you have say structured data like HTML, ASCII, JSON, etc which often have lots of ASCII data mixed in with meant-for-human strings which may be non-ASCII, but it's very bursty.
As you point out, for your algorithm, 50% ASCII but no burstiness basically means every vector takes the non-ASCII path, which is actually "good" compared to say 50% of vectors taking the ASCII shortcut (which would slow things down due to BP failures) - so a lot of the interesting design space is ignored (e.g., I think the ideal algorithm will choose between strategies in a branch-predictor aware fashion).
> this doesn't matter very much for benchmarking, at least for the mostly-branchless SIMD algorithms like mine
Right - but of course it matters a lot for existing algorithms (which tend to be branchy), which can come out looking much worse or much better depending on the distributions (not that I think any will be able to pass a good vectorized approach). Also, as above, the early-out for ASCII will matter for a lot of practical inputs, but neither benchmark stresses it.
> I doubt there's much speed to be gained by adding any sort of adaptive behavior beyond the ASCII check. Generally these days you want as few branches as you can manage, and this algorithm has only one that matters.
Right, well the ASCII one is a big one. I haven't looked at the details of your algorithm, but could the core loop be faster if say it never saw any 4-byte sequences, or certain other uncommon things? Then an adaptive approach would use an optimized kernel for that scenario if it was encountered for a while.
"Adaptive" doesn't really mean more branches - just that you occasionally might switch to a different kernel based on the observed data. This shouldn't add many branches compared to e.g., the existing loop and failure branches, and evidently you have to do it in a branch-predictor aware way (e.g., you need some type of hysteresis in mode switches so you don't switch too often). Sometimes you can build the adaptivity into the existing branches, e.g., maybe you are taking a branch anyways (e.g., when you find non-ASCII text) and you can build the adaptive "state machine" directly into the code via duplication of code, w/o needing any explicit data counting the number of failures (effectively, the IP holds extra information recording something about the path you took to reach the current location).
Ah, thanks for the reply. I understand you better now, and for the most part I agree. Taking out the early exit for ASCII can speed up the frequently-changing case to avoid the misprediction penalty, as long as sufficiently many chunks of input need to take the long validation path.
Thinking about this more, I think there's at least one way that an adaptive approach might be beneficial without too much extra complexity: keeping two copies of the main kernel for ASCII-checking and non-ASCII checking. It should be pretty cheap to add a counter that is incremented or decremented based on the ASCII-only mask, and split the outer loop into N-byte chunks, with a branch for each chunk determining whether we should take the branchless path or not.
> Right, well the ASCII one is a big one. I haven't looked at the details of your algorithm, but could the core loop be faster if say it never saw any 4-byte sequences, or certain other uncommon things? Then an adaptive approach would use an optimized kernel for that scenario if it was encountered for a while.
I started out thinking this wouldn't really work, but I think there might be some potential here. My initial problem was that even if some work could be saved if there weren't 4 byte sequences, we still need to detect them every time. But, because my algorithm uses lookup tables for error flags which have some free bits, and because the 4 byte sequences can be detected in the same indices that are used for these lookups, we can set another error bit that means "take the 4-byte slow path". Then, when some input fails validation, we only do the work there to check whether it's really a failure. This gets complicated, though: first off, the check for the proper number of continuation bytes is before the table lookup, so we'd need to put some logic in there. Secondly, this lookup table gets validation failures one byte later in the stream than the initial byte. So in the case that the 4-byte sequence starts on the last byte of a vector, we'd need to have special handling.
It'd probably be a good idea to also have some hysteresis in this approach too... So overall, I think there might be some nice gains from adaptive behavior. There are two big concerns that make me skeptical, though: code size/I$ pressure, and code complexity. While code size wouldn't be much of an issue in microbenchmarks, in real applications it matters a lot more--I don't want the size overhead to get too big. Right now the full validation algorithm is about 600 bytes of code for AVX2, I'd rather not make that explode by a large factor by adding several specializations. And I'm already reaching the limits of what little generic programming can be done in the C preprocessor... This sort of specialization is really better done with C++ templates (or maybe Rust or something). I had wanted to keep this a pure-C library, but maybe C++ might be better.
Hope some of that makes sense... I'm mostly thinking out loud here. In any case, you've given me a lot to think about, so thanks very much!
> If you know of any good UTF-8 corpora for common use cases, I'd be happy to benchmark them.
I think a reasonable approach is the first N bytes of wikipedia for various interesting languages which stress the UTF-8 continuum, e.g., English, French, maybe something in Cyrillic, Greek, Korean, Japanese, Mandarin.
Those are in html IIRC so you should have a good mix of ASCII plus UTF-8.
Definitely a good idea. I had already downloaded a Mandarin Wikipedia page to look for test sequences, but hadn't benchmarked them. Getting some more pages there is a great place to start.
I don't have any particular plans for that, beyond getting a bit of publicity for it here :). Or really any plans at all, besides adding AVX-512 support. It was just something I hacked together in a couple days as I began thinking about adding UTF-8 support to the text editor I've been writing.
That said, I'm happy to help anyone get it integrated into their codebase if they need it. There's generally other concerns in large projects, though, like portability, that I don't care as much about.
There's not as big a niche as you might think. A lot of the software that needs valid UTF-8 don't validate upfront, but do it as part of a parsing process. An example would be something like an XML or JSON parser.
For more mundane uses like opening a big text file, you often want to tolerate invalid bytes and show a replacement character. You could use the technique, but you'd have to modify it to work for that usage.
[re: upfront parsing - a PSA about validating input]
> A lot of the software ... don't validate upfront
They almost always should be validating all input up front. Deferring validation tends to become many different pieces of code all informally parsing fragments of the input that are needed locally. Without upfront validation of the complete unit of input, the resulting fragmented parsers are just a weird machine waiting to be programmed by a malicious attacker.
For a much better explanation, I strongly recommend Meredith and Sergey's 28c3 talk[1] about The Science of Insecurity.
> you often want to tolerate invalid bytes and show a replacement character
While this validator wouldn't be useful in that situation, it is still important to validate the input upfront. When showing replacement characters, the "invalid bytes" that will be supported with a replacement character should be formally defined and added to the validation grammar, because they are no longer "invalid", but instead will be handled as a special case.
Postel's robustness principle shouldn't be used as an excuse to skip validation. Being "liberal in what you accept" should still be well-defined and validated.
In case anyone sees this: I initially agreed with you, thinking that pure UTF-8 validation is a bit more of a niche than might be expected. But if I'm not mistaken, I think at least for JSON validation, the UTF-8 validation can happen completely independently of the parsing. I don't think any control characters (braces, commas, quotes, escapes, etc) will change the validity of the UTF, or vice versa. So it might be beneficial (both for speed and security, as the sibling comment notes) to validate chunks of input as UTF-8, then pass them off to the parser, which now doesn't need to deal with validation.
Maybe this could be applied to XML, but that's quite a behemoth of a standard--I really wouldn't be surprised if there was a way to switch encodings mid-stream. I have no idea though...
Sure, you can validate and then parse the JSON, but parsing it is also effectively validating it as UTF-8, except for the string values in the JSON, so it's heavily going to be duplicated effort. And unfortunately, the strings can contain things like invalid surrogate pairs by using unicode escapes, so you may want to validate after escaping.
You can't normally blindly parse XML as UTF-8. The encoding has rules for detecting the character set.
> Anyone have any ideas about which open source codebases UTF-8 validators exist in?
Rust's std library -- the canonical way to read a text file to a string is to (implicitly) use `std::str::from_utf8`. If I remember correctly, the current implementation doesn't use SIMD specificially but will of course contain vectored instructions if the compiler can select them on the platform you target.
On an i7-7800X (clang 6.0.0-1ubuntu2, WSL, don't trust my numbers) my benchmarks showed about the same relationship between SSE4, AVX2 and Lemire's code, as your benchmarks did. My own attempt is about half as fast. https://raw.githubusercontent.com/andrewffff/utf8fuzz/2019_c...
A few examples of invalid UTF-8 from Markus Kuhn's suite pass this validator right now, specifically 4.1.3, 4.2.3 and 4.3.3. My randomized tests, which compare the results from different validators, also fail a small percentage of the time, I'd guess for the same reason. https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
I'm really interested in the different approaches taken here. Fortunately both Daniel and you've communicated what you were doing, I think it's going to take longer for me to re-comprehend my own approach!
Oh wow, that is most definitely a bug. Thank you very much for reporting that, before this spreads too much. How embarrassing... I was in a bit of a rush to stick this up on HN before the weekend, as otherwise I'd probably never get around to it. Evidently I got a bit sloppy making the error table.
Luckily, that's a very easy bug to fix--it was just caused by a mistake constructing the error tables. The only annoying part is having to renumber the error bits and write the tables again by hand :)
Thanks too for benchmarking! I see you tried out make.py, but it didn't work? I should probably add a Makefile for people that don't want to deal with yet another random build system...
55 comments
[ 65.2 ms ] story [ 348 ms ] threadA small nitpick: not sure whether those macros bought anything, I guess the optimizer could inline function calls to intrinsic wrappers just fine as well?
That'd sure be neat! I really have no idea, though.
> A small nitpick: not sure whether those macros bought anything, I guess the optimizer could inline function calls to intrinsic wrappers just fine as well?
Yeah, that's a bit weird. It's my hacky generics-in-C method, that allows both AVX2 and SSE4 implementations to be compiled in one codebase, while keeping the base algorithm clean. The idea is that you can compile both versions by including multiple times, like so:
...which would allow a later runtime dispatch based on CPUID, etc.It looks like the main naive validator there (validate_utf8) clocks in at 1.25 cycles/byte for ASCII and 11 cycles/byte for UTF-8, which is respectively a 15.8x and 41.5x slowdown from my code.
Since when saved megawatts bothered programmers? If that was the case we would see far smaller usage of scripting languages and electron apps.
Why do you believe this? I don't think it is quite that ubiquitous.
To switch existing code to use some particular UTF-8 decoding library should be as simple as changing some imports, and perhaps some other (search/replace) code tweaks or implementing a quick shim/adapter. If it doesn't work out, rollback or abandon the branch. On most codebases, even larger ones, this whole process is probably an easier thing to do, requiring less effort than finding a decent (and more power efficient) alternative to Electron — seems to be the GP's gist. And personally I'd agree.
Yup that's right. Any sign of inconvenience and efficiency be damned. Why do we even bother trying to fix environment. This cost money and inconveniences so many people.
Also it depends on your background. I have no problem doing without Electron. I would have to apply your exact logic to even look at it.
> Any sign of inconvenience and efficiency be damned.
Nope, not any signs... just that if you can build more with less time, in exchange of a bit of CPU efficiency, it's not so bad.
> Why do we even bother trying to fix environment.
For the future. You see, my program using a bit more RAM or CPU won't change much. Not eating that meat, that will change quite a bit, thus that's one of the things I do.
A software can allow people to be many orders in magnitudes more efficient, if I can build more of them, it allows even more people to be a few orders of magnitude more efficient. The power usage afterward, fit in the error margin quite well.
Obviously, a more efficient language with a better algorithm and good caching is best, but often we don't have the resources (whether it be time or money or whatever) to engineer the best solution, so we need to choose where to focus carefully. Sometimes that means faster prototyping in a language that may not be quite as efficient, and then using the extra time to iterate on algorithmic changes that may yield major benefits or put a caching layer in place.
Except all those spaces between the # and “define”, no sir I don’t like that :)
Thanks though! I like taking the time to make my code as clean as I can. Glad others appreciate it!
After that, the answer to "does this pull request comply with my formatting guidelines?" gets reduced to "does clang-format change this file?"
> This algorithm should map fairly nicely to AVX-512, and should in fact be a bit faster than 2x the speed of AVX2 since a few instructions can be saved
In general, you aren't going to make a faster wheel without some trickery (vectorization usually). All the obvious optimizations have already been made in the stdlibs.
https://www.reddit.com/r/rust/comments/dx6e0h/comment/f7o8av...
TL;DR; yes but not too much if your CPU is high-end and if the number of AVX instructions is low enough (and the slowdown is negligible in front of the gains if the number of instructions is high).
To get downclocking to L1 license (the middle speed tier), you need to use either FMA unit AVX/AVX2 insructions or any AVX-512 instructions. This algo doesn't use any of this, staying in the integer domain.
Here's a longer description:
https://stackoverflow.com/a/56861355
Of course this doesn't address say power or temperature based throttling which might be more likely to occur when wide SIMD is used, but it's not possible to give a precise answer there, other than many low or moderate core count setups won't see this kind of throttling outside of extreme use cases.
> v_load(data + offset + V_LEN - 1);
Other than that, this is incredibly clean code and I doubt you can get much more performance without dropping into assembly. I wish my coworkers made code this well documented :/
Thanks for the kind words, though! It warms my heart :)
I don't remember last time when a compiler did not do this for me. Compilers are generally pretty smart nowadays.
The key to a solid claim, especially for something as bold as "fastest in the world", is a complete specification of the inputs to the benchmark.
You mention random ASCII bytes and "random UTF-8 bytes". The former is definitely an really important case but also the least interesting. I can write on a napkin a UTF-8-but-is-actually-ASCII decoder that approaches 256 bytes a cycle cached (with a fallback routine when the ASCII assumption fails).
So then what about the random UTF-8 bytes. What does it mean? Do you generate random bytes and then exclude invalid sequences? Do you generate a uniform random codepoint in the 21-bit codepoint space and then concert it to utf-8?
At a minimum it would good to see a benchmark with random distributions that approximate common languages.
The true fastest decoders on such realistic data will be adaptive and more complicated than yours.
The random UTF-8 in the benchmark was generated from the code in Daniel Lemire's fastvalidate-utf-8 repository, specifically this code: https://github.com/lemire/fastvalidate-utf-8/blob/ed53c0c64b...
Looking closer at it, I think that the distribution of random UTF-8 is not as uniform as it should be: it generates one byte first, and then generates continuation bytes depending on the value of that byte. Which means that half the code points will just be ASCII.
But I think this doesn't matter very much for benchmarking, at least for the mostly-branchless SIMD algorithms like mine. For each vector of input bytes, there's three branches that can get taken: the early exit for ASCII-only, and two branches that exit the loop due to validation failures, which don't really matter for benchmarking. Even though the distribution of code points will be roughly half ASCII, for a 32-byte AVX2 vector, the probability of a pure ASCII vector is something less than 2^-32 (since non-ASCII initial bytes translate into more than one byte of output, and the probability of 32 ASCII code points in a row is 2^-32, the probability of 32 bytes in a row is less, by an amount I'd rather not try to calculate). So for this particular benchmark, random UTF-8 should be roughly equivalent to "no ASCII". To verify, I disabled generating ASCII in the linked code, and the numbers came out pretty much identical. If anything, even more ASCII bytes would be a more interesting test, since that would make the quick-ASCII branch less predictable. If you know of any good UTF-8 corpora for common use cases, I'd be happy to benchmark them.
Given that different sets of "random UTF-8 bytes" generated with this method will virtually always follow the same path, the exact distribution doesn't really matter when measuring cycles/byte. Input that is purely 2-byte code points will be faster in terms of cycles/code point than purely 4-byte input, but that's mostly a property of UTF-8 being variable length.
I doubt there's much speed to be gained by adding any sort of adaptive behavior beyond the ASCII check. Generally these days you want as few branches as you can manage, and this algorithm has only one that matters.
Indeed, this is a good example of a problem with random bechmarks: no text really has the behavior of uniform random with 50% ASCII but no character-to-character correlation. In the real world you often expect bursts of ASCII, e.g., where a title is written in ASCII or where you have say structured data like HTML, ASCII, JSON, etc which often have lots of ASCII data mixed in with meant-for-human strings which may be non-ASCII, but it's very bursty.
As you point out, for your algorithm, 50% ASCII but no burstiness basically means every vector takes the non-ASCII path, which is actually "good" compared to say 50% of vectors taking the ASCII shortcut (which would slow things down due to BP failures) - so a lot of the interesting design space is ignored (e.g., I think the ideal algorithm will choose between strategies in a branch-predictor aware fashion).
> this doesn't matter very much for benchmarking, at least for the mostly-branchless SIMD algorithms like mine
Right - but of course it matters a lot for existing algorithms (which tend to be branchy), which can come out looking much worse or much better depending on the distributions (not that I think any will be able to pass a good vectorized approach). Also, as above, the early-out for ASCII will matter for a lot of practical inputs, but neither benchmark stresses it.
> I doubt there's much speed to be gained by adding any sort of adaptive behavior beyond the ASCII check. Generally these days you want as few branches as you can manage, and this algorithm has only one that matters.
Right, well the ASCII one is a big one. I haven't looked at the details of your algorithm, but could the core loop be faster if say it never saw any 4-byte sequences, or certain other uncommon things? Then an adaptive approach would use an optimized kernel for that scenario if it was encountered for a while.
"Adaptive" doesn't really mean more branches - just that you occasionally might switch to a different kernel based on the observed data. This shouldn't add many branches compared to e.g., the existing loop and failure branches, and evidently you have to do it in a branch-predictor aware way (e.g., you need some type of hysteresis in mode switches so you don't switch too often). Sometimes you can build the adaptivity into the existing branches, e.g., maybe you are taking a branch anyways (e.g., when you find non-ASCII text) and you can build the adaptive "state machine" directly into the code via duplication of code, w/o needing any explicit data counting the number of failures (effectively, the IP holds extra information recording something about the path you took to reach the current location).
Thinking about this more, I think there's at least one way that an adaptive approach might be beneficial without too much extra complexity: keeping two copies of the main kernel for ASCII-checking and non-ASCII checking. It should be pretty cheap to add a counter that is incremented or decremented based on the ASCII-only mask, and split the outer loop into N-byte chunks, with a branch for each chunk determining whether we should take the branchless path or not.
> Right, well the ASCII one is a big one. I haven't looked at the details of your algorithm, but could the core loop be faster if say it never saw any 4-byte sequences, or certain other uncommon things? Then an adaptive approach would use an optimized kernel for that scenario if it was encountered for a while.
I started out thinking this wouldn't really work, but I think there might be some potential here. My initial problem was that even if some work could be saved if there weren't 4 byte sequences, we still need to detect them every time. But, because my algorithm uses lookup tables for error flags which have some free bits, and because the 4 byte sequences can be detected in the same indices that are used for these lookups, we can set another error bit that means "take the 4-byte slow path". Then, when some input fails validation, we only do the work there to check whether it's really a failure. This gets complicated, though: first off, the check for the proper number of continuation bytes is before the table lookup, so we'd need to put some logic in there. Secondly, this lookup table gets validation failures one byte later in the stream than the initial byte. So in the case that the 4-byte sequence starts on the last byte of a vector, we'd need to have special handling.
It'd probably be a good idea to also have some hysteresis in this approach too... So overall, I think there might be some nice gains from adaptive behavior. There are two big concerns that make me skeptical, though: code size/I$ pressure, and code complexity. While code size wouldn't be much of an issue in microbenchmarks, in real applications it matters a lot more--I don't want the size overhead to get too big. Right now the full validation algorithm is about 600 bytes of code for AVX2, I'd rather not make that explode by a large factor by adding several specializations. And I'm already reaching the limits of what little generic programming can be done in the C preprocessor... This sort of specialization is really better done with C++ templates (or maybe Rust or something). I had wanted to keep this a pure-C library, but maybe C++ might be better.
Hope some of that makes sense... I'm mostly thinking out loud here. In any case, you've given me a lot to think about, so thanks very much!
I think a reasonable approach is the first N bytes of wikipedia for various interesting languages which stress the UTF-8 continuum, e.g., English, French, maybe something in Cyrillic, Greek, Korean, Japanese, Mandarin.
Those are in html IIRC so you should have a good mix of ASCII plus UTF-8.
Anyone have any ideas about which open source codebases UTF-8 validators exist in?
That said, I'm happy to help anyone get it integrated into their codebase if they need it. There's generally other concerns in large projects, though, like portability, that I don't care as much about.
For more mundane uses like opening a big text file, you often want to tolerate invalid bytes and show a replacement character. You could use the technique, but you'd have to modify it to work for that usage.
> A lot of the software ... don't validate upfront
They almost always should be validating all input up front. Deferring validation tends to become many different pieces of code all informally parsing fragments of the input that are needed locally. Without upfront validation of the complete unit of input, the resulting fragmented parsers are just a weird machine waiting to be programmed by a malicious attacker.
For a much better explanation, I strongly recommend Meredith and Sergey's 28c3 talk[1] about The Science of Insecurity.
> you often want to tolerate invalid bytes and show a replacement character
While this validator wouldn't be useful in that situation, it is still important to validate the input upfront. When showing replacement characters, the "invalid bytes" that will be supported with a replacement character should be formally defined and added to the validation grammar, because they are no longer "invalid", but instead will be handled as a special case.
Postel's robustness principle shouldn't be used as an excuse to skip validation. Being "liberal in what you accept" should still be well-defined and validated.
[1] https://media.ccc.de/v/28c3-4763-en-the_science_of_insecurit...
How unicode replacement characters should work is already well defined. Just look it up.
Maybe this could be applied to XML, but that's quite a behemoth of a standard--I really wouldn't be surprised if there was a way to switch encodings mid-stream. I have no idea though...
You can't normally blindly parse XML as UTF-8. The encoding has rules for detecting the character set.
Rust's std library -- the canonical way to read a text file to a string is to (implicitly) use `std::str::from_utf8`. If I remember correctly, the current implementation doesn't use SIMD specificially but will of course contain vectored instructions if the compiler can select them on the platform you target.
I did a comparison with another SIMD based implementation last year. Maybe it's time to update it: https://github.com/killercup/simd-utf8-check
The whole algorithm basically has to be redesigned from scratch to introduce vectorization: something far beyond the capabilities of the compiler.
[0] https://www.joelonsoftware.com/2003/10/08/the-absolute-minim...
https://utf8everywhere.org/
On an i7-7800X (clang 6.0.0-1ubuntu2, WSL, don't trust my numbers) my benchmarks showed about the same relationship between SSE4, AVX2 and Lemire's code, as your benchmarks did. My own attempt is about half as fast. https://raw.githubusercontent.com/andrewffff/utf8fuzz/2019_c...
A few examples of invalid UTF-8 from Markus Kuhn's suite pass this validator right now, specifically 4.1.3, 4.2.3 and 4.3.3. My randomized tests, which compare the results from different validators, also fail a small percentage of the time, I'd guess for the same reason. https://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
I'm really interested in the different approaches taken here. Fortunately both Daniel and you've communicated what you were doing, I think it's going to take longer for me to re-comprehend my own approach!
Thanks for sharing this.
Luckily, that's a very easy bug to fix--it was just caused by a mistake constructing the error tables. The only annoying part is having to renumber the error bits and write the tables again by hand :)
Thanks too for benchmarking! I see you tried out make.py, but it didn't work? I should probably add a Makefile for people that don't want to deal with yet another random build system...