AMD与Intel的Unicode性能对决:AVX-512基准测试深度解析

本文通过simdutf库的UTF-16到UTF-8转码测试,对比AMD EPYC 9R45与Intel Xeon 6975P-C处理器的AVX-512指令集性能。AMD以11GB/s的吞吐量显著超越Intel的6GB/s,展现了在SIMD指令执行效率上的架构优势。

AMD与Intel:Unicode性能基准测试

通俗来说,我们的处理器分为两种类型:手机中的ARM处理器和Intel与AMD制造的x64处理器。最好的服务器处理器过去由Intel制造。但如今,Intel越来越难以跟上竞争步伐。

最近,亚马逊提供了最新的AMD微架构(Zen 5)。具体来说,如果您启动一个r8a实例,您将获得AMD EPYC 9R45处理器。对应的Intel实例(r8i)则配备Intel Xeon 6975P-C处理器。这款Intel处理器属于Granite Rapids家族(2024年)。

Phoronix的Michael Larabel有几篇关于新AMD处理器的文章。其中一篇题为《AMD EPYC 9005带来惊人性能》。这篇文章非常值得一读。他发现,与之前的AMD处理器(采用Zen 4微架构)相比,AMD EPYC 9R45速度快了1.6倍。在第二篇文章中,Michael将AMD处理器与相应的Intel处理器进行了比较。他发现AMD处理器比Intel处理器快1.6倍。

我决定亲自测试一下。我正好在准备simdutf库的新版本发布。simdutf库支持UTF-8、UTF-16和UTF-32编码之间的快速转码,以及其他功能。它被主要浏览器和JavaScript运行时(如Node.js或Bun)使用。一个常见且重要的操作是从UTF-16到UTF-8的转换。JavaScript内部使用UTF-16,因此大多数字符使用2字节,而互联网默认使用UTF-8,字符可以使用1到4字节。

UTF-16是一种可变长度Unicode编码,使用单个16位代码单元(值从0x0000到0xd7ff和0xe000到0xffff)表示大多数常见字符,但通过使用代理对扩展到U+FFFF以上的完整Unicode范围:一个高代理(0xd800到0xdbff)后跟一个低代理(0xdc00到0xdfff),共同编码一个单个补充字符,映射到四个UTF-8字节。因此,我们可以认为代理对中的每个元素在UTF-8中占两个字节。范围0x0000到0x007f(ASCII)的非代理代码单元变为一个字节,0x0080到0x07ff变为两个字节,0x0800到0xffff(不包括代理)变为三个字节。

我的基准测试代码首先确定需要多少输出内存,然后进行转码。

1
2
3
4
5
size_t utf8_length = simdutf::utf8_length_from_utf16(str.data(), str.size());
if(buffer.size() < utf8_length) {
  buffer.resize(utf8_length);
}
simdutf::convert_utf16_to_utf8(str.data(), str.size(), buffer.data());

在最近的处理器上,转码代码并不简单(Clausecker和Lemire,2023)。然而,从UTF-16数据计算UTF-8长度稍微简单一些。

这些Intel和AMD处理器支持AVX-512指令:这些指令可以操作最多64字节的寄存器,相比我们通常使用的64位寄存器。这是SIMD的一个实例:单指令多数据。使用AVX-512,您可以一次加载和处理32个UTF-16单元。我们的主要例程如下所示。

1
2
3
4
5
6
7
8
9
__m512i input = _mm512_loadu_si512(in);
__mmask32 is_surrogate = _mm512_cmpeq_epi16_mask(
_mm512_and_si512(input, _mm512_set1_epi16(0xf800)),
_mm512_set1_epi16(0xd800));
__mmask32 c0 = _mm512_test_epi16_mask(input, _mm512_set1_epi16(0xff80));
__mmask32 c1 = _mm512_test_epi16_mask(input, _mm512_set1_epi16(0xf800));
count += count_ones32(c0);
count += count_ones32(c1);
count -= count_ones32(is_surrogate);

代码处理从内存加载的512位UTF-16代码单元向量,使用_mm512_loadu_si512。然后通过首先应用按位AND(_mm512_and_si512)用0xf800掩码每个代码单元,仅保留高五位,并将结果(_mm512_cmpeq_epi16_mask)与0xd800比较;这产生一个32位掩码,其中位为任何在代理范围(0xd800到0xdfff)内的代码单元设置,指示潜在的UTF-16代理对,这些代理对不应在UTF-8中贡献额外长度。接下来,我们使用按位测试检查(_mm512_test_epi16_mask)每个代码单元对0xff80的掩码,为任何非ASCII代码单元在c0中设置位。类似地,另一个_mm512_test_epi16_mask函数对0xf800在c1中为需要3字节UTF-8的代码单元(代理对除外)设置位。最后,代码累积c0和c1中设置位的数量到计数器,然后减去代理掩码的popcount。总体而言,我们可以使用大约十二条指令处理约32个UTF-16单元。(感谢Wojciech Muła的有见地的设计,以及Yagiz Nizipli帮助我进行相关优化。)

配备AMD处理器的大型亚马逊实例价格为0.13892美元/小时,而Intel处理器为0.15976美元/小时。我使用Amazon Linux启动了这两个实例。然后在shell中运行以下命令。

1
2
3
4
5
6
7
sudo yum install cmake git gcc
sudo dnf install gcc14 gcc14-c++
git clone https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog.git
cd Code-used-on-Daniel-Lemire-s-blog/2025/11/15
CXX=gcc14-g++ cmake -B build
cmake --build build
./build/benchmark

我得到以下结果。

处理器 GB/s GHz 指令/字节 指令/周期
AMD 11 4.5 1.7 4.0
Intel 6 3.9 1.7 2.6

基准测试结果显示,在UTF-16到UTF-8转码中,AMD处理器的吞吐量几乎是Intel处理器的两倍(10.53 GB/s对比5.96 GB/s),部分得益于其更高的运行频率。两个系统都需要相同的每字节1.71指令,但AMD实现了显著更高的每周期指令数(3.98 i/c对比2.64 i/c),展示了在AVX-512流水线中的卓越执行效率。其中一个原因与执行单元的数量有关。AMD处理器有四个能够处理512位寄存器的计算单元,而Intel通常仅限于两个这样的执行单元。

我的基准测试比Larabel的更狭窄,它们有助于显示在使用AVX-512指令时,AMD相对于Intel具有巨大优势。考虑到Intel发明了AVX-512而AMD较晚支持,这一点尤其引人注目。可以说AMD在Intel的游戏中击败了Intel。

我的基准测试代码可用。

进一步阅读:Robert Clausecker, Daniel Lemire, Transcoding Unicode Characters with AVX-512 Instructions, Software: Practice and Experience 53 (12), 2023。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计