我已经使用亚马逊最新的 ARM 处理器(graviton 3)几周了。据我所知,这些是支持可扩展矢量扩展 ( SVE ) 的第一个广泛可用的处理器。
SVE 是单指令/多数据范式的一部分:一条指令可以一次对多个值进行操作。因此,例如,您可以使用一条指令将 N 个整数与 N 个其他整数相加。
SVE 的独特之处在于您使用值向量,但不知道向量的具体长度。这与向量的大小是硬编码的传统 SIMD 指令(ARM NEON、x64 SSE、AVX)形成对比。您不仅在不知道向量大小的情况下编写代码,甚至编译器也可能不知道。这意味着相同的二进制可执行文件可以在不同的数据块(向量)上工作,具体取决于处理器。这种方法的好处是您的代码在新处理器上可能会神奇地变得更加高效。
这是一个大胆的提议。即使我们有相同的指令集,也有可能编写在一个处理器上工作但在另一个处理器上失败的代码。
但是 graviton 3 处理器上的 SVE 速度快吗?为了测试它,我写了一个小基准。假设您想从数组中删除所有负整数。教科书的实现可能如下所示:
无效remove_negatives_scalar ( const int32_t *输入, int64_t 计数, int32_t *输出) { int64_t 我= 0 ; int64_t j = 0 ; 对于( ; i <计数; i + + ) { 如果(输入[我] > = 0 ) { 输出[ j + + ] =输入[ i ] ; } } }
但是,编译器可能会生成一个分支,如果您的输入具有随机分布,这可能是低效的代码。为了帮助解决问题,您可以以更有可能生成无分支二进制文件的方式重写您的代码:
对于( ; i <计数; i + + ) { 输出[ j ] =输入[ i ] ; j + = (输入[ i ] > = 0 ) ; }
尽管它看起来效率较低(因为每个输入值都被写出),但这种无分支版本通常实际上更快。
我使用 ARM 内部函数将最后一个实现移植到 SVE。在每一步,我们加载一个整数向量( svld1_s32 ),我们将它们与零进行比较( svcmpge_n_s32 ),我们删除负值( svcompact_s32 )并存储结果( svst1_s32 )。在大多数迭代中,我们有一个完整的整数向量……然而,在最后一次迭代中,一些值会丢失,但我们只是用while_mask变量忽略它们,该变量指示哪些整数值是“活动的”。整个代码序列完全使用 SVE 指令完成:无需像传统 SIMD 指令集那样单独处理序列的结尾。
#包括< arm_sve.h > void remove_negatives (常量int32_t *输入, int64_t计数, int32_t *输出) { int64_t 我= 0 ; int64_t j = 0 ; svbool_t while_mask = svwhilelt_b32 (我,计数) ; 做{ svint32_t in = svld1_s32 ( while_mask ,输入+ i ) ; svbool_t 正= svcmpge_n_s32 ( while_mask , in , 0 ) ; svint32_t in_positive = svcompact_s32 (正,在) ; svst1_s32 ( while_mask ,输出+ j , in_positive ) ; i + = svcntw ( ) ; j + = svcntp_b32 ( while_mask ,正) ; while_mask = svwhilelt_b32 (我,计数) ; }而( svptest_any ( svptrue_b32 ( ) , while_mask ) ) ; }
在我的基准测试中使用 graviton 3 处理器和 GCC 11,我得到以下结果:
周期/整数 | 指令/整数 | 指令/周期 | |
---|---|---|---|
标量 | 9.0 | 6.000 | 0.7 |
无分支标量 | 1.8 | 8.000 | 4.4 |
SVE | 0.7 | 1.125 | 1.6 |
SVE 代码使用的指令要少得多。在这个特定的测试中,SVE 比最好的竞争对手(无分支标量)快 2.5 倍。此外,随着底层寄存器变得更宽,它可能在未来的处理器上使用更少的指令。
当然,我的代码肯定不是最理想的,但我很高兴我编写的第一个 SVE 基准测试结果如此出色。这表明 SVE 在实践中可能会做得很好。