如 cmets 中所述,最快的代码很可能使用标量运算,在整数寄存器中执行所有操作。你需要做的就是提取四个压缩的 64 位整数,然后你有三个 XOR 指令,你就完成了。这可以非常有效地完成,并将结果留在整数寄存器中,这正是您的示例代码所暗示的。
MSVC 已经为您在问题中作为示例显示的标量函数生成了非常好的代码:
inline uint64_t HorizontalXor(__m256i t) {
return t.m256i_u64[0] ^ t.m256i_u64[1] ^ t.m256i_u64[2] ^ t.m256i_u64[3];
}
假设t 在ymm1 中,反汇编结果如下:
vextractf128 xmm0, ymm1, 1
vpextrq rax, xmm0, 1
vmovq rcx, xmm1
xor rax, rcx
vpextrq rcx, xmm1, 1
vextractf128 xmm0, ymm1, 1
xor rax, rcx
vmovq rcx, xmm0
xor rax, rcx
...结果留在RAX。如果这准确地反映了您的需求(标量 uint64_t 结果),那么这段代码就足够了。
你可以稍微通过使用内在函数来改进它:
inline uint64_t _mm256_hxor_epu64(__m256i x)
{
const __m128i temp = _mm256_extracti128_si256(x, 1);
return (uint64_t&)x
^ (uint64_t)(_mm_extract_epi64(_mm256_castsi256_si128(x), 1))
^ (uint64_t&)(temp)
^ (uint64_t)(_mm_extract_epi64(temp, 1));
}
然后你会得到以下反汇编(再次假设x在ymm1中):
vextracti128 xmm2, ymm1, 1
vpextrq rcx, xmm2, 1
vpextrq rax, xmm1, 1
xor rax, rcx
vmovq rcx, xmm1
xor rax, rcx
vmovq rcx, xmm2
xor rax, rcx
请注意,我们能够省略一条提取指令,并且我们已确保使用 VEXTRACTI128 而不是 VEXTRACTF128(尽管 this choice probably does not matter)。
您会在其他编译器上看到类似的输出。例如,这里是 GCC 7.1(x 假定在 ymm0 中):
vextracti128 xmm2, ymm0, 0x1
vpextrq rax, xmm0, 1
vmovq rdx, xmm2
vpextrq rcx, xmm2, 1
xor rax, rdx
vmovq rdx, xmm0
xor rax, rdx
xor rax, rcx
那里有相同的说明,但它们已经稍微重新排序。内在函数允许编译器的调度程序按照它认为最好的方式进行排序。 Clang 4.0 以不同的方式安排它们:
vmovq rax, xmm0
vpextrq rcx, xmm0, 1
xor rcx, rax
vextracti128 xmm0, ymm0, 1
vmovq rdx, xmm0
xor rdx, rcx
vpextrq rax, xmm0, 1
xor rax, rdx
当然,当代码被内联时,这个顺序总是会发生变化。
另一方面,如果您希望将结果存储在 AVX 寄存器中,那么您首先需要决定如何存储它。我猜您只会将单个 64 位结果存储为标量,例如:
inline __m256i _mm256_hxor(__m256i x)
{
const __m128i temp = _mm256_extracti128_si256(x, 1);
return _mm256_set1_epi64x((uint64_t&)x
^ (uint64_t)(_mm_extract_epi64(_mm256_castsi256_si128(x), 1))
^ (uint64_t&)(temp)
^ (uint64_t)(_mm_extract_epi64(temp, 1)));
}
但现在您正在执行大量数据混洗,从而抵消了您可能通过矢量化代码看到的任何性能提升。
说到这一点,我真的不知道你是如何让自己陷入需要首先进行这样的横向操作的情况的。 SIMD 操作旨在垂直 缩放,而不是水平缩放。如果您仍处于实施阶段,重新考虑设计可能是合适的。特别是,您应该在 4 个不同的 AVX 寄存器中生成 4 个整数值,而不是将它们全部打包成一个。
如果您确实希望将结果的 4 个副本 打包到 AVX 寄存器中,那么您可以执行以下操作:
inline __m256i _mm256_hxor(__m256i x)
{
const __m256i temp = _mm256_xor_si256(x,
_mm256_permute2f128_si256(x, x, 1));
return _mm256_xor_si256(temp,
_mm256_shuffle_epi32(temp, _MM_SHUFFLE(1, 0, 3, 2)));
}
这仍然通过一次执行两个 XOR 来利用一点并行性,这意味着总共只需要两个 XOR 操作,而不是三个。
如果它有助于将其可视化,这基本上可以:
A B C D ⟵ input
XOR XOR XOR XOR
C D A B ⟵ permuted input
=====================================
A^C B^D A^C B^D ⟵ intermediate result
XOR XOR XOR XOR
B^D A^C B^D A^C ⟵ shuffled intermediate result
======================================
A^C^B^D A^C^B^D A^C^B^D A^C^B^D ⟵ final result
在几乎所有编译器上,这些内在函数都会生成以下汇编代码:
vperm2f128 ymm0, ymm1, ymm1, 1 ; input is in YMM1
vpxor ymm2, ymm0, ymm1
vpshufd ymm1, ymm2, 78
vpxor ymm0, ymm1, ymm2
(我在第一次发布这个答案后在睡觉的路上想出了这个,并计划回来更新答案,但我看到 wim 在发布它时击败了我。哦,好吧,它仍然比我第一次使用的方法更好,因此仍然值得将其包含在此处。)
当然,如果你想在整数寄存器中使用它,你只需要一个简单的VMOVQ:
vperm2f128 ymm0, ymm1, ymm1, 1 ; input is in YMM1
vpxor ymm2, ymm0, ymm1
vpshufd ymm1, ymm2, 78
vpxor ymm0, ymm1, ymm2
vmovq rax, xmm0
问题是,这会比上面的标量代码更快吗?答案是,是的,可能。尽管您使用 AVX 执行单元进行 XOR,而不是完全独立的整数执行单元,但需要执行的 AVX 洗牌/置换/提取更少,这意味着更少的开销。所以我可能还不得不承认标量代码是最快的实现。但这实际上取决于您在做什么以及如何安排/交错指令。