如果您拥有具有高效SIMD指令的CPU,SSE/MMX
paddb
(
_mm_add_epi8
)也是可行的。
Peter Cordes' answer还描述了GNU C (gcc/clang)向量语法,并且对于严格别名 UB 的安全性进行了说明。我强烈建议您也查看该答案。
使用
uint64_t
自己完成完全可移植,但仍需要小心以避免访问
uint64_t*
中的
uint8_t
数组时出现对齐问题和严格别名 UB。您通过在
uint64_t
中开始使用数据而将该部分省略,但对于GNU C,
may_alias
typedef可以解决该问题(请参见Peter的答案或
memcpy
)。
否则,您可以将数据分配/声明为
uint64_t
,并在需要单个字节时通过
uint8_t*
访问它。
unsigned char*
允许别名任何内容,因此针对8位元素的特定情况可以避开这个问题。(如果根本不存在
uint8_t
,那么很可能可以安全地假设它是一个
unsigned char
。)
请注意,这是从之前错误的算法更改而来(请参见修订历史记录)。
对于任意减法,不需要循环即可实现,并且在每个字节中使用已知常数(如
1
)会更有效率。主要技巧是通过设置高位来防止每个字节的进位,然后纠正减法结果。
我们将稍微优化给出的减法技巧
here。它们定义:
SWAR sub z = x - y
z = ((x | H) - (y &~H)) ^ ((x ^~y) & H)
将H
定义为0x8080808080808080U
(即每个打包整数的MSB)。对于递减,y
是0x0101010101010101U
。
我们知道y
的所有MSB都已清除,因此我们可以跳过一个掩码步骤(即y&~H
在我们的情况下与y
相同)。计算如下:
- 我们将
x
的每个组件的MSB设置为1,这样借位就无法传播到下一个组件。称之为调整后的输入。
- 我们从矫正后的输入中减去每个分量的1,通过减去
0x01010101010101
来实现。由于步骤1,这不会导致分量间的借位。称之为调整后的输出。
- 我们现在需要更正结果的MSB。我们将调整后的输出与原始输入的反转MSB异或以完成修复结果。
该操作可写为:
#define U64MASK 0x0101010101010101U
#define MSBON 0x8080808080808080U
uint64_t decEach(uint64_t i){
return ((i | MSBON) - U64MASK) ^ ((i ^ MSBON) & MSBON);
}
最好由编译器内联(使用编译器指令强制执行),或将表达式作为另一个函数的一部分内联编写。
测试用例:
in: 0000000000000000
out: ffffffffffffffff
in: f200000015000013
out: f1ffffff14ffff12
in: 0000000000000100
out: ffffffffffff00ff
in: 808080807f7f7f7f
out: 7f7f7f7f7e7e7e7e
in: 0101010101010101
out: 0000000000000000
性能细节
这里是单次调用函数的x86_64汇编代码。为了获得更好的性能,应该将其内联,希望常量尽可能长时间地存储在寄存器中。在常量存储在寄存器中的紧密循环中,实际的减少需要五个指令:优化后的或+非+与+加+xor。我没有看到比编译器优化更好的替代方案。
uint64t[rax] decEach(rcx):
movabs rcx, -9187201950435737472
mov rdx, rdi
or rdx, rcx
movabs rax, -72340172838076673
add rax, rdx
and rdi, rcx
xor rdi, rcx
xor rax, rdi
ret
通过对以下代码片段进行IACA测试:
// Repeat the SWAR dec in a loop as a microbenchmark
uint64_t perftest(uint64_t dummyArg){
uint64_t dummyCounter = 0;
uint64_t i = 0x74656a6d27080100U; // another dummy value.
while(i ^ dummyArg) {
IACA_START
uint64_t naive = i - U64MASK;
i = naive + ((i ^ naive ^ U64MASK) & U64MASK);
dummyCounter++;
}
IACA_END
return dummyCounter;
}
我们可以展示,在Skylake机器上,执行减法、异或和比较跳转操作只需要不到5个时钟周期:
Throughput Analysis Report
Block Throughput: 4.96 Cycles Throughput Bottleneck: Backend
Loop Count: 26
Port Binding In Cycles Per Iteration:
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
| Cycles | 1.5 0.0 | 1.5 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.5 | 1.5 | 0.0 |
(当然,在x86-64上,您只需加载或使用
movq
将其加载到XMM寄存器中进行
paddb
,因此更有趣的是看看它在像RISC-V这样的ISA上如何编译。)