如果我给程序员一个字符串,例如“9223372036854775808” ,并要求他们将其转换为整数,他们可能会在 C++ 中执行以下操作:
std ::字符串s = . 。 。 。 uint64_t 值; 自动[ ptr , ec ] = std :: from_chars ( s . data ( ) , s . data ( ) + s . size ( ) , val ) ; if ( ec ! = std :: errc ( ) ) { } // 我有一个错误! // val 保存值
它非常快:您可以使用大约 128 条指令以每个整数大约 40 个周期来解析随机 32 位整数序列。
你能做得更好吗?
最近的英特尔处理器有新的指令 AVX-512,它可以一次处理多个字节并进行屏蔽,以便您可以仅选择一系列数据。
我假设您知道数字序列的开头和结尾。以下带有 AVX-512 内在函数的代码执行以下操作:
- 计算以字节为单位的跨度 (digit_count),
- 如果我们有超过 20 个字节,我们就知道该整数太大,无法容纳 64 位整数,
- 我们计算一个“掩码”:一个 32 位值,只有最低有效的 digital_count 位设置为 1,
- 我们将 ASCII 或 UTF-8 字符串加载到 256 位寄存器中,
- 我们减去字符值“0”以获得 0 到 9 之间的值(数字值),
- 我们检查某个值是否超过 9,在这种情况下我们有一个非数字字符。
size_t数字计数= size_t (结束-开始) ; // if (digit_count > 20) { 错误 ....} const simd8x32 ASCII_ZERO = _mm256_set1_epi8 ( '0' ) ; const simd8x32 九= _mm256_set1_epi8 ( 9 ) ; uint32_t 掩码= uint32_t ( 0xFFFFFFFF ) < < (开始-结束+ 32 ) ; 自动输入= _mm256_maskz_loadu_epi8 (掩码,结束-32 ) ; 自动base10_8bit = _mm256_maskz_sub_epi8 (掩码, in , ASCII_ZERO ) ; 自动非数字= _mm256_mask_cmpgt_epu8_mask (掩码, base10_8bit ,九) ; if (非数字) { // 有一个非数字 }
这是使用 AVX-512 功能的关键步骤。之后,对于熟悉传统 x64 处理器上的高级 Intel 内在函数的人来说,我们可以使用“老式”处理……大多数情况下,我们只需乘以 10、乘以 100、乘以 100000 即可创建四个 32 位值:第一个对应于最低有效的 8 个 ASCII 字节,第二个到下一个最高有效的 8 个 ASCII 字节,以及最多 4 个最高有效字节。当数字为 8 位或更少时,只有其中一个单词相关;当数字为 16 位或更少时,前两个单词有意义。我们总是浪费一个由零组成的 32 位值。代码可能如下所示:
自动DIGIT_VALUE_BASE10_8BIT = _mm256_set_epi8 ( 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 , 1 , 10 ) ; 自动DIGIT_VALUE_BASE10E2_8BIT = _mm_set_epi8 ( 1 , 100 , 1 , 100 , 1 , 100 , 1 , 100 , 1 , 100 , 1 , 100 , 1 , 100 , 1 , 100 ) ; 自动DIGIT_VALUE_BASE10E4_16BIT = _mm_set_epi16 ( 1 , 10000 , 1 , 10000 , 1 , 10000 , 1 , 10000 ) ; 自动基10e2_16位= _mm256_maddubs_epi16 ( base10_8bit , DIGIT_VALUE_BASE10_8BIT ) ; 自动base10e2_8bit = _mm256_cvtepi16_epi8 ( base10e2_16bit ) ; 自动基10e4_16位= _mm_maddubs_epi16 ( base10e2_8bit , DIGIT_VALUE_BASE10E2_8BIT ) ; 自动基10e8_32位= _mm_madd_epi16 ( base10e4_16bit , DIGIT_VALUE_BASE10E4_16BIT ) ;
我用C++实现了这个函数,并使用GCC12编译它。我在 Ice Lake 服务器上运行基准测试。我使用随机 32 位整数进行测试。 AVX-512 的速度是标准方法的两倍多。
AVX-512 | 1.8GB/秒 | 57 指令/数量 | 17 周期/次数 |
std::from_chars | 0.8GB/秒 | 128条指令/数量 | 39 周期/次数 |
目前的比较并不完全公平,因为 AVX-512 函数假设它知道数字序列的开头和结尾。
您可以通过使用内联函数来提高性能,使其达到 2.3 GB/s,性能提升 30%。但是,假设您正在循环内按顺序解析数字。
我的原始代码将返回奇特的 std::Optional 值,但 GCC 受到负面影响,因此我将函数签名更改为更常规。甚至,在我的测试中,与 GCC 相比,LLVM/clang 稍微快一些。
信用:原始代码和问题是 John Keiser 向我建议的。我的代码很大程度上是他的代码的衍生。
原文: https://lemire.me/blog/2023/09/22/parsing-integers-quickly-with-avx-512/