处理器大致分为英特尔和 AMD 的两大系列 x64 处理器,以及苹果、三星和许多其他供应商的 ARM 处理器。长期以来,ARM 处理器主要占据嵌入式处理器(在家里运行冰箱的计算机)市场,而“大处理器”几乎完全是 x64 处理器的领域。
据报道,在苹果设计 iPhone 时,苹果 CEO (史蒂夫乔布斯) 去见英特尔,以寻求处理器交易。英特尔拒绝了苹果。所以苹果选择了 ARM。
今天,我们将 ARM 处理器用于一切:游戏机(Nintendo Switch)、强大的服务器(亚马逊和谷歌)、手机、嵌入式设备等等。
亚马逊推出了其基于 ARM 的新型处理器(Graviton 3)。这些处理器具有复杂的 SIMD 指令(SIMD 代表单指令多数据),称为 SVE(可扩展向量扩展)。有了这些指令,我们可以大大加速软件。它是单核并行的一种形式,与通过对一个任务使用多个内核获得的并行相反。 SIMD 并行性在适用时通常比多核并行性高效得多。
Amazon 的 Graviton 3 似乎有 32 字节的寄存器,因为它最适合ARM Neoverse V1 设计。您可以在一个寄存器中容纳八个 32 位整数。主流 ARM 处理器(例如,英特尔使用的处理器)也具有 SIMD 指令(NEON),但寄存器较短(16 字节)。拥有更宽的寄存器和能够在这些宽寄存器上运行的指令可以减少指令的总数。执行更少的指令是加速代码的一种非常好的方法。
为了研究 SVE,我研究了一个简单的问题,您想从数组中删除所有负整数。也就是说,您读取包含有符号随机整数的数组,并且您只想将正整数写入输出数组。正常的 C 代码可能如下所示:
无效remove_negatives_scalar ( const int32_t *输入, int64_t 计数, int32_t *输出) { int64_t 我= 0 ; int64_t j = 0 ; 对于( ; i <计数; i + + ) { 如果(输入[我] > = 0 ) { 输出[ j + + ] =输入[ i ] ; } } }
用依赖特殊 SVE 函数的新代码替换此代码使其运行速度更快(快 2.5 倍) 。当时,我建议我的代码可能不是最优的。它每次循环迭代处理 32 个字节,使用 9 条指令。这 9 条指令中有很大一部分与管理循环有关,很少有人进行实际的数字运算。一位读者 Samuel Lee 提议有效地展开我的循环。由于较低的循环开销,他预测性能会更好(至少在数组足够大时)。我在下面包括了他提出的代码。
在我的基准测试中使用 graviton 3 处理器和 GCC 11,我得到以下结果:
循环/整数 | 指令/int | 指令/周期 | |
---|---|---|---|
标量 | 9.0 | 6.000 | 0.7 |
无分支标量 | 1.8 | 8.000 | 4.4 |
SVE | 0.7 | 1.125 | ~1.6 |
展开的 SVE | 0.4385 | 0.71962 | ~1.6 |
新展开的 SVE 代码使用大约 23 条指令来处理 128 个字节(或 32 个 32 位整数),因此每个整数大约有 0.71875 条指令。就 CPU 周期而言,这比标量代码少大约 10 倍的指令,比标量代码快大约 4 倍。
两个 SVE 函数每周期引退的指令数大致相同,并且相对较低,略高于每个周期引退 1.5 条指令。
通常支持 SVE 的论点是它不需要特殊的代码来完成处理的尾部。也就是说,您可以使用 SVE 指令处理整个数组,即使它的长度不能被寄存器大小(这里是 8 个整数)整除。我发现 Lee 的代码很有趣,因为它说明了出于效率原因,您实际上可能需要以不同的方式处理长数组的末尾。
总的来说,我认为我们可以看到 SVE 可以很好地解决手头的问题(过滤掉 32 位整数)。
附录:Samuel Lee 的代码。
void remove_negatives (常量int32_t *输入, int64_t 计数, int32_t *输出) { int64_t j = 0 ; 常量int32_t * endPtr =输入+计数; 常量uint64_t vl_u32 = svcntw ( ) ; svbool_t all_mask = svptrue_b32 ( ) ; 而(输入< = endPtr-( 4 * vl_u32 ) ) { svint32_t in0 = svld1_s32 ( all_mask ,输入+ 0 * vl_u32 ) ; svint32_t in1 = svld1_s32 ( all_mask ,输入+ 1 * vl_u32 ) ; svint32_t in2 = svld1_s32 ( all_mask ,输入+ 2 * vl_u32 ) ; svint32_t in3 = svld1_s32 ( all_mask ,输入+ 3 * vl_u32 ) ; svbool_t pos0 = svcmpge_n_s32 ( all_mask , in0 , 0 ) ; svbool_t pos1 = svcmpge_n_s32 ( all_mask , in1 , 0 ) ; svbool_t pos2 = svcmpge_n_s32 ( all_mask , in2 , 0 ) ; svbool_t pos3 = svcmpge_n_s32 ( all_mask , in3 , 0 ) ; in0 = svcompact_s32 ( pos0 , in0 ) ; in1 = svcompact_s32 ( pos1 , in1 ) ; in2 = svcompact_s32 ( pos2 , in2 ) ; in3 = svcompact_s32 ( pos3 , in3 ) ; svst1_s32 ( all_mask ,输出+ j , in0 ) ; j + = svcntp_b32 ( all_mask , pos0 ) ; svst1_s32 ( all_mask ,输出+ j , in1 ) ; j + = svcntp_b32 ( all_mask , pos1 ) ; svst1_s32 ( all_mask ,输出+ j , in2 ) ; j + = svcntp_b32 ( all_mask , pos2 ) ; svst1_s32 ( all_mask ,输出+ j , in3 ) ; j + = svcntp_b32 ( all_mask , pos3 ) ; 输入+ = 4 * vl_u32 ; } int64_t 我= 0 ; 计数= endPtr -输入; 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 ) ) ; }
原文: https://lemire.me/blog/2022/07/14/filtering-numbers-faster-with-sve-on-amazon-graviton-3-processors/