您的手机可能运行在 64 位 ARM 处理器上。这些处理器无处不在:它们为 Nintendo Switch 提供动力,为 Amazon AWS 和 Microsoft Azure 的云服务器提供动力,为快速笔记本电脑提供动力,等等。 ARM 处理器具有称为 ARM NEON 的特殊强大指令。它们提供了一种称为单指令多数据 (SIMD) 的特定类型的并行性。例如,您可以使用一条指令将 16 个值与其他 16 个值相加。
使用称为内在函数的特殊函数,您可以编写一个 C 函数,将两个数组中的值相加并将结果写入第三个数组:
#包含< arm_neon.h > 无效向量_int_add ( int32_t * a , int32_t * b , int32_t *结果, int len ) { 整数i = 0 ; // 一次循环处理 4 个元素,直到我们不能再处理为止 for ( ; i < len - 3 ; i + = 4 ) { // 将 'a' 中的 4 个 32 位整数加载到 NEON 寄存器中 int32x4_t vec_a = vld1q_s32 ( a + i ) ; // 将 'b' 中的 4 个 32 位整数加载到另一个 NEON 寄存器中 int32x4_t vec_b = vld1q_s32 ( b + i ) ; // 执行向量加法 int32x4_t vec_res = vaddq_s32 ( vec_a , vec_b ) ; // 将结果存回内存 vst1q_s32 (结果+ i , vec_res ) ; } // 处理所有不能被 4 整除的剩余元素 for ( ; i < len ; i ++ ) { 结果[ i ] = a [ i ] + b [ i ] ; } }
在此代码中, int32x4_t 类型表示四个 32 位整数。您可以类似地将 16 个 8 位整数表示为 int8x16_t 等等。
内部函数vld1q_s32 和 vst1q_s32 加载和存储 16 字节数据。内部函数 vaddq_s32 进行求和。
一个简单但看似棘手的任务是确定寄存器是否仅包含零。这在算法中经常出现。不幸的是,与 Intel/AMD 指令集(AVX2、AVX-512)不同,ARM NEON 中没有相应的指令。
ARM NEON 有许多不同的有效方法,但它们都需要多条指令。为了简单起见,我假设您正在接收 16 个 8 位整数 (uint8x16_t)。实际上,您可以将 16 个字节重新解释为您喜欢的任何内容(例如,从 uint8x16_t 到int32x4_t),而无需任何成本。
我最喜欢的方法是利用 ARM NEON 可以计算 SIMD 寄存器中的最大值或最小值这一事实:内在 vmaxvq_u32(对应于指令 umaxv)计算向量所有元素的最大值并将其作为标量返回(不是向量)。根据您的数据类型,还有其他变体,例如 vmaxvq_u8,但 vmaxvq_u32 往往是性能最高的方法。代码可能如下所示:
int veq_non_zero_max ( uint8x16_t v ) { 返回vmaxvq_u32 ( vreinterpretq_u32_u8 ( v ) ) ! = 0 ; }
它编译为三个基本指令:umaxv、fmov 和比较 (cmp)。我们需要 fmov 指令或等效指令将数据从 SIMD 寄存器移动到标量寄存器。整体代码并不好:umaxv 至少有三个周期的延迟,fmov 也是如此。
有一种更复杂但可能更有用的基于缩小偏移的方法。 vshrn_n_u16 内在函数(对应于 shrn 指令)将 8 个 16 位整数右移 4 位,并将结果缩小到 8 位。结果是一个 64 位值,其中包含原始 16 字节寄存器中每个字节的 4 个最高有效位。我们可以检查寄存器是否为零,如下所示:
int veq_non_zero_narrow ( uint8x16_t v ) { 返回vget_lane_u64 ( vshrn_n_u16 ( vreinterpretq_u16_u8 ( v ) , 4 ) , 0 ) ! = 0 ; }
它编译为三个指令:shrn、fmov 和比较。速度并没有更快。
罗伯特·克劳塞克向我指出了一种更快的方法。我们可以利用 SIMD 寄存器也用作浮点寄存器的事实,而不是将数据从 SIMD 寄存器移动到标量寄存器。因此我们可以将数据保留在原处。它适用于目前提出的两种技术( vmaxvq_u32 和vshrn_n_u16)。这是 vshrn_n_u16 的版本:
int veq_non_zero_float ( uint8x16_t v ) { uint8x8_t 缩小= vshrn_n_u16 ( vreinterpretq_u16_u8 ( v ) , 4 ) ; return ( vdupd_lane_f64 ( vreinterpret_f64_u16 (缩小) , 0 ) ! = 0.0 ) ; }
这仅编译为两条指令:shrn 和 fcmp。因此速度要快得多。
那么为什么不使用浮点方法呢?
- 我们有两个不同的值 0.0 和 -0.0,它们被认为是相等的。
- 浮点标准包括称为次正常值的微小值,在某些配置下可以将其视为等于零。
- 我们可能会生成一个信号 NaN 值,这可能会导致发出信号。
如果你小心的话,你可以避免所有这些问题,但这并不能使它成为一个好的通用解决方案。
较新的 64 位 ARM 处理器具有另一个指令集系列:SVE 或标量向量扩展。据我所知,它不能直接与 ARM NEON 互操作……但它确实有一个由以下代码生成的专用指令(cmpeq):
布尔check_all_zeros ( svbool_t 掩码, svint8_t vec ) { svbool_t cmp = svcmpeq_n_s8 (掩码, vec , 0 ) ; 返回svptest_any ( pg , cmp ) ; }
SVE 代码更加复杂:它需要一个掩码来指示哪些值是“活动的”。要将所有值设置为活动状态,您可能需要生成一个真实掩码:例如,像“svbool_t pg = svptrue_b8()”一样。然而,它更强大:您可以检查所有活动值是否为零……
不幸的是,SVE 尚未普及。
原文: https://lemire.me/blog/2025/01/20/checking-whether-an-arm-neon-register-is-zero/