我在内存中有大量的64位值。不幸的是,它们可能未与64位地址对齐。我的目标是更改所有这些值的字节序,即交换/反转它们的字节。
我知道bswap
指令,该指令交换32或64位寄存器的字节。但是由于它需要一个寄存器参数,因此我无法将其传递给我的内存地址。当然,我可以先将内存加载到寄存器中,然后交换,然后将其写回:
mov rax, qword [rsi]
bswap rax
mov qword [rsi], rax
但是考虑到地址可能未对齐,这甚至是正确的吗?
另一种可能性是手动进行交换:
mov al, byte [rsi + 0]
mov bl, byte [rsi + 7]
mov byte [rsi + 0], bl
mov byte [rsi + 7], al
mov al, byte [rsi + 1]
mov bl, byte [rsi + 6]
mov byte [rsi + 1], bl
mov byte [rsi + 6], al
mov al, byte [rsi + 2]
mov bl, byte [rsi + 5]
mov byte [rsi + 2], bl
mov byte [rsi + 5], al
mov al, byte [rsi + 3]
mov bl, byte [rsi + 4]
mov byte [rsi + 3], bl
mov byte [rsi + 4], al
显然还有更多说明。但是它也慢吗?
但是总的来说,我仍然对x86-64缺乏经验,所以我想知道:字节交换内存中64位值的最快方法是什么?我描述的两个选项之一是最优的吗?还是有完全不同的方法甚至更快?
PS:我的实际情况更加复杂。我的确有一个大字节数组,但它包含大小不同的整数,且都密集地打包。其他一些数组告诉我接下来要等待的整数大小。因此,此“描述”可以说“一个32位int,两个64位int,一个16位int,然后再一个64位int”。我只是在这里提到这是为了告诉您(据我所知),无法使用SIMD指令,因为实际上我必须在读取之前检查每个整数的大小。
什么是最快的字节交换内存中64位值的方法?
mov/bswap/mov
版本和movbe/mov
在大多数Intel处理器上大致相同。基于µop计数,除了Atom上,似乎movbe
解码为mov + bswap
。对于Ryzen,movbe
可能更好。手动在字节周围交换要慢得多。
[pshufb
是一个合理的选择,即使替换单个bswap
,尽管浪费了改组可能完成的工作的一半。
PS:我的实际情况更加复杂。我的确有一个大字节数组,但它包含大小不同的整数,所有整数都紧密堆积。
在这种一般情况下,随着从其他数据流中动态获取大小,一个新的大问题就是分支了。即使在可以避免的标量代码中,也可以通过字节反转64位块并将其右移8 - size
,然后将其与未反转的字节合并,然后前进size
。可以解决这个问题,但是尝试浪费时间,SIMD版本会更好。
SIMD版本可以使用pshufb
和由“大小模式”索引的混洗掩码表,例如一个8位整数,其中每2位表示元素的大小。然后pshufb
反转它正在查看的16字节窗口中完全包含的元素,而将其余部分保留下来(那些未更改的尾部字节也将被写回,但这没关系)。然后我们前进实际处理的字节数。
为了最大的方便,这些大小模式(以及相应的字节数)应以这样的方式提供:实际的Endianness Flipper本身可以在每次迭代中完全使用其中的一个,而不会造成诸如[[extracting] 8位字节不对齐的序列,并动态确定要消耗多少位。这也是可能的,但是成本要高得多。在我的测试中,速度大约慢了4倍,受循环携带的依赖性的限制,方法是“从当前位索引处提取8位”,“通过查找表查找位索引增加”,然后进入下一个迭代:每个迭代约16个周期,尽管仍相当于等量标量代码所花费时间的60%。使用解压缩的(每个大小1个字节)表示形式将使提取更加容易(只是未对齐的dword负载),但是需要打包结果以使用pext
来索引混洗掩码表。这对于Intel CPU来说是合理的,但是pext
在AMD Ryzen上非常慢。一个对AMD和Intel都很好的替代方法是读取未对齐的dword,然后使用乘法/移位技巧提取8个有趣的位:
mov eax, [rdi]
imul eax, eax, 0x01041040
shr eax, 24
应该使用的额外技巧是在存储当前迭代结果之前读取下一次迭代的数据。没有这种技巧,存储将经常“踩脚步”下一次迭代的负载(因为我们前进不到16个字节,因此负载读取了存储保持不变但无论如何都必须写的一些字节),迫使它们之间存在存储器依赖性,从而阻止了下一次迭代。性能差异很大,约为3倍。
那么Endianness Flipper可能看起来像这样:
void flipEndiannessSSSE3(char* buffer, size_t totalLength, uint8_t* sizePatterns, uint32_t* lengths, __m128i* masks)
{
size_t i = 0;
size_t j = 0;
__m128i data = _mm_loadu_si128((__m128i*)buffer);
while (i < totalLength) {
int sizepattern = sizePatterns[j];
__m128i permuted = _mm_shuffle_epi8(data, masks[sizepattern]);
size_t next_i = i + lengths[j++];
data = _mm_loadu_si128((__m128i*)&buffer[next_i]);
_mm_storeu_si128((__m128i*)&buffer[i], permuted);
i = next_i;
}
}
例如,带有-O3 -march=haswell
的Clang 10将变成
test rsi, rsi
je .LBB0_3
vmovdqu xmm0, xmmword ptr [rdi]
xor r9d, r9d
xor r10d, r10d
.LBB0_2: # =>This Inner Loop Header: Depth=1
movzx eax, byte ptr [rdx + r10]
shl rax, 4
vpshufb xmm1, xmm0, xmmword ptr [r8 + rax]
mov eax, dword ptr [rcx + 4*r10]
inc r10
add rax, r9
vmovdqu xmm0, xmmword ptr [rdi + rax]
vmovdqu xmmword ptr [rdi + r9], xmm1
mov r9, rax
cmp rax, rsi
jb .LBB0_2
.LBB0_3:
ret
[LLVM-MCA认为每次迭代大约需要3.3个周期,在我的PC(4770K,用1,2,4,4和8字节大小的元素的均匀混合进行测试)上,它稍微慢一点,每次迭代接近3.7个周期,但这仍然很好:每个元素不到1.2个周期。