对于 RISC-V,您可能正在使用 GCC/clang。
有趣的事实:GCC 知道其中一些 SWAR bithack 技巧(显示在其他答案中),并且可以在使用GNU C 本机向量为没有硬件 SIMD 指令的目标编译代码时为您使用它们。(但是 RISC-V 的 clang 只会天真地将其展开为标量操作,因此如果您想要跨编译器的良好性能,您必须自己做)。
本机向量语法的一个优点是,当针对具有硬件 SIMD 的机器时,它将使用它而不是自动向量化您的 bithack 或类似的东西。
它使编写vector -= scalar
操作变得容易;语法 Just Works,隐式广播,也就是为你喷出标量。
另请注意,uint64_t*
来自 a 的负载uint8_t array[]
是严格混叠 UB,所以要小心。(另请参阅为什么 glibc 的 strlen 需要如此复杂才能快速运行? re:在纯 C 中使 SWAR bithacks 严格混叠安全)。你可能想要这样的东西来声明一个uint64_t
你可以指针转换来访问任何其他对象的东西,比如char*
在 ISO C/C++ 中的工作方式。
使用这些将 uint8_t 数据放入 uint64_t 以用于其他答案:
// GNU C: gcc/clang/ICC but not MSVC
typedef uint64_t aliasing_u64 __attribute__((may_alias)); // still requires alignment
typedef uint64_t aliasing_unaligned_u64 __attribute__((may_alias, aligned(1)));
执行别名安全加载的另一种方法是使用memcpy
into a uint64_t
,这也删除了alignof(uint64_t
) 对齐要求。但是在没有有效未对齐负载的 ISA 上,gcc/clang 在memcpy
无法证明指针对齐时不会内联和优化,这将对性能造成灾难性影响。
TL:DR:您最好的选择是将数据声明为uint64_t array[...]
或动态分配为uint64_t
,或者最好alignas(16) uint64_t array[];
确保与至少 8 个字节对齐,或者如果您指定 16 个字节alignas
。
由于uint8_t
几乎可以肯定unsigned char*
,因此访问uint64_t
via的字节是安全的uint8_t*
(但对于 uint8_t 数组,反之则不然)。因此,对于窄元素类型为 的这种特殊情况unsigned char
,您可以回避严格混叠问题,因为char
它很特殊。
GNU C 本机向量语法示例:
GNU C 原生向量总是被允许使用它们的底层类型来别名(例如int __attribute__((vector_size(16)))
可以安全地别名int
但不能float
或uint8_t
其他任何东西。
#include <stdint.h>
#include <stddef.h>
// assumes array is 16-byte aligned
void dec_mem_gnu(uint8_t *array) {
typedef uint8_t v16u8 __attribute__ ((vector_size (16), may_alias));
v16u8 *vecs = (v16u8*) array;
vecs[0] -= 1;
vecs[1] -= 1; // can be done in a loop.
}
对于没有任何硬件 SIMD 的 RISC-V,您可以使用它vector_size(8)
来表达您可以有效使用的粒度,并执行两倍的较小向量。
但是vector_size(8)
使用 GCC 和 clang 为 x86 编译非常愚蠢:GCC 在 GP 整数寄存器中使用 SWAR bithacks,clang 解压缩为 2 字节元素以填充 16 字节 XMM 寄存器,然后重新打包。(MMX 已经过时了,以至于 GCC/clang 甚至都懒得使用它,至少对于 x86-64 来说不是。)
但是使用vector_size (16)
( Godbolt ) 我们得到了预期的movdqa
/ paddb
。(使用由 生成的全为向量pcmpeqd same,same
)。由于-march=skylake
我们仍然得到两个单独的 XMM 操作而不是一个 YMM,所以不幸的是,当前的编译器也没有将向量操作“自动向量化”为更广泛的向量:/
对于 AArch64,使用vector_size(8)
(Godbolt)还不错;ARM/AArch64 可以在 8 或 16 字节块中使用d
或q
寄存器本机工作。
vector_size(16)
因此,如果您想要跨 x86、RISC-V、ARM/AArch64 和 POWER 的便携性能,您可能想要实际编译。但是,其他一些 ISA 在 64 位整数寄存器中执行 SIMD,例如我认为的 MIPS MSA。
vector_size(8)
更容易查看 asm(只有一个寄存器的数据):Godbolt compiler explorer
# GCC8.2 -O3 for RISC-V for vector_size(8) and only one vector
dec_mem_gnu(unsigned char*):
lui a4,%hi(.LC1) # generate address for static constants.
ld a5,0(a0) # a5 = load from function arg
ld a3,%lo(.LC1)(a4) # a3 = 0x7F7F7F7F7F7F7F7F
lui a2,%hi(.LC0)
ld a2,%lo(.LC0)(a2) # a2 = 0x8080808080808080
# above here can be hoisted out of loops
not a4,a5 # nx = ~x
and a5,a5,a3 # x &= 0x7f... clear high bit
and a4,a4,a2 # nx = (~x) & 0x80... inverse high bit isolated
add a5,a5,a3 # x += 0x7f... (128-1)
xor a5,a4,a5 # x ^= nx restore high bit or something.
sd a5,0(a0) # store the result
ret
我认为这与其他非循环答案的基本思想相同;防止进位然后修复结果。
这是 5 条 ALU 指令,比我认为的最佳答案差。但看起来关键路径延迟只有 3 个周期,有两条 2 条指令链,每条链都通向异或。@Reinstate Monica - ζ-- 的答案编译为 4 周期 dep 链(对于 x86)。5 周期循环吞吐量因在关键路径上还包含一个幼稚sub
而成为瓶颈,并且循环确实在延迟上成为瓶颈。
但是,这对clang没有用。它甚至没有按照加载的顺序添加和存储,所以它甚至没有做好的软件流水线!
# RISC-V clang (trunk) -O3
dec_mem_gnu(unsigned char*):
lb a6, 7(a0)
lb a7, 6(a0)
lb t0, 5(a0)
...
addi t1, a5, -1
addi t2, a1, -1
addi t3, a2, -1
...
sb a2, 7(a0)
sb a1, 6(a0)
sb a5, 5(a0)
...
ret