当编译器在 UTF-8 问题上产生分歧时
When Compilers Disagree About UTF‑8

原始链接: https://nemanjatrifunovic.substack.com/p/when-compilers-disagree-about-utf8

2006年,作者创建了一个开源C++ UTF-8库。最近,他试图通过为ASCII字符添加专门的快速路径分支来优化其UTF-8解码过程。 在Clang上的初步测试结果令人印象深刻:纯ASCII文本的吞吐量翻了三倍,混合文本的性能也提升了34%。然而,GCC在处理ASCII时毫无提升,处理混合内容时性能甚至略有下降。 经调查汇编代码后,作者发现GCC的优化器已经有效地移除了对ASCII的验证检查,使得手动优化显得多余。相反,Clang并未进行此类优化,这意味着手动修改带来了显著的提升。 为了确保一致性,作者重构了代码,将验证逻辑以内联方式放入解码函数中。这种方法令两款编译器都表现良好,不仅保持了Clang上的收益,还在GCC上实现了15%至20%的混合文本性能提升。这一经验提醒我们,编译器行为差异巨大,深入研究生成的汇编代码对于有效的性能调优至关重要。

这篇 Hacker News 讨论聚焦于如何优化 UTF-8 处理。用户 *kstenerud* 建议使用 SIMD 指令批量处理 ASCII 文本,以避开逐字节循环。他还提出了一种基于码点字节长度“聚类”的优化策略,认为如果数据流使用 3 字节编码,那么可以将状态机优化为预期类似的模式。 *duskwuff* 则提出了细致的反驳,指出此类优化高度依赖于上下文。由于大多数语言都依赖 ASCII 范围内的字符来实现基本语法(如空格、标点符号和换行符),因此数据流会持续处于特定多字节范围内的假设,在通用文本处理中可能并不成立。
相关文章

原文

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.

联系我们 contact @ memedata.com