对于RISC-V,您可能正在使用GCC / clang。
有趣的事实:GCC知道其中一些SWAR bithack技巧(在其他答案中显示),并且在使用GNU C本机向量为没有硬件SIMD指令的目标编译代码时,可以使用它们。(但是RISC-V的clang只会天真地将其展开为标量运算,因此,如果您希望跨编译器具有良好的性能,则必须自己进行操作)。
本机向量语法的一个优点是,当针对具有硬件SIMD 的计算机时,它将使用该向量,而不是自动向量化您的bithack或类似的恐怖内容。
它使编写vector -= scalar
操作变得容易。语法Just Works,也为您隐式广播标量。
另请注意,uint64_t*
来自uint8_t array[]
严格别名UB 的负载,因此要小心。(另请参见为什么要快速运行glibc的原因如此复杂? 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
到中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
孔的字节是安全的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 bithack,将clang解压缩为2个字节的元素以填充16个字节的XMM寄存器,然后重新打包。(MMX已过时,以至于GCC / clang甚至都不会使用它,至少对于x86-64来说不是这样。)
但随着vector_size (16)
(Godbolt),我们得到预期的movdqa
/ paddb
。(使用生成的全矢量pcmpeqd same,same
)。由于-march=skylake
我们仍然得到两个单独的XMM ops而不是一个YMM,因此不幸的是,当前的编译器也不会将矢量ops“自动矢量化”为更宽的矢量:/
对于AArch64,使用起来还不错vector_size(8)
(Godbolt);ARM / AArch64可以使用d
或q
寄存器以8或16字节的块形式进行本地工作。
因此vector_size(16)
,如果您希望在x86,RISC-V,ARM / AArch64和POWER上具有可移植的性能,则可能需要实际进行编译。但是,其他一些ISA在64位整数寄存器中执行SIMD,例如我认为的MIPS MSA。
vector_size(8)
使查看asm更加容易(只有一个寄存器值的数据):Godbolt编译器资源管理器
# 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条指令各自导致XOR。@Reinstate Monica-ζ--的答案编译为一个4周期的dep链(对于x86)。通过sub
在关键路径上包含朴素的内容,可以使5周期循环的吞吐量成为瓶颈,而循环确实使延迟成为瓶颈。
但是,这对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