In the summer of 2006 I started an open-source C++ library for handling UTF-8 strings. I wanted it to be portable and to work well with STL but did not go crazy with optimizations at the time.
Recently, as I started writing about UTF-8 decoding, I spent more time revisiting the internals of the library. The function I decided to optimize decodes a UTF-8 encoded code point:
Pretty straightforward: based on the value of the lead byte, determine the length of the sequence, and then depending on the length, construct the value of the code point by extracting appropriate bit fields from the bytes. After the code point is successfully decoded, perform two more checks: one for validity of the code point, and another for an overlong UTF-8 sequence. If both checks pass, return the success status, and the iterator is moved to the next sequence.
An opportunity to optimization was the fact that ASCII trivially satisfies all UTF‑8 validity requirements. If it turns out the high bit of the lead byte is zero, we have an “ASCII character” - a value in the range of [U+0000, U+007F] which is always valid. All we need to do is zero-extend it and we have our code point.
Something like this:
My expectation was to see a visible improvement in handling of sequences of ASCII-only strings, with a minor impact on mixed ASCII/non-ASCII strings. Testing with clang 18.1.3 beat my expectations: For pure ASCII text, UTF-8 decoding throughput tripled, and even for mixed text, the improvement was substantial: around 34%.
I actually submitted the change to GitHub, but then at some point decided to test with gcc. Compilers just never stop surprising me: this time there was no difference for the ASCII text at all! Zero!
For heavily mixed text the throughput was consistently worse by 3-4%, which was not that surprising.
Time to get our hands dirty and look at the generated assembly code. Here is what happens for an ASCII code point with original version compiled with g++:
This is an optimized, heavily inlined build and the last two instructions come from a function calling validate_next in a loop. But it is pretty clear that there are no validation checks in the ASCII branch: the compiler was able to figure out the checks always pass and simply removed them. My optimization did not change a thing - generated code for the ASCII branch remains the same.
Now clang:
Obviously, clang did not eliminate code point validity checks. With my optimization, the code becomes:
That certainly explains the huge performance improvement with clang.
What I ended up with was a somewhat bigger change that satisfied both compilers. Instead of checking for code point validity and overlong sequences after decoding is done, I enhanced the get_sequence_* functions to perform validation inline. That preserved all performance gains on clang and led to 15-20% of improvement with gcc and mixed text (no difference for pure ASCII).
Moral of the story? Compilers are tricky, but spending some time on optimizations can be rewarding.