我正在研究 .NET Core 3.0 在 System.Runtime.Intrinsics 命名空间中对硬件内在函数的新支持。
我有一些代码在循环中执行 4 个异或运算 - 下面是一个简化的示例(我不是在 IDE 中编写的,所以请忽略任何语法错误:
private static unsafe ulong WyHashCore(byte[] array)
{
fixed (byte* pData = array)
{
byte* ptr = pData;
// Consume 32-byte chunks
for (int i = 0; i < array.Length; i += 32)
{
ulong a = Read64(ptr, i);
ulong b = Read64(ptr, i + 8);
ulong c = Read64(ptr, i + 16);
ulong d = Read64(ptr, i + 24);
// XOR them with some constants
ulong xor1 = a ^ SOME_CONSTANT1;
ulong xor2 = b ^ SOME_CONSTANT2;
ulong xor3 = c ^ SOME_CONSTANT3;
ulong xor4 = d ^ SOME_CONSTANT4;
// Use the resulting values
}
}
}
Read64
方法如下所示:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe ulong Read64(byte* ptr, int start)
=> *(ulong*)(ptr + start);
我尝试将 4 行异或行替换为:
byte[] array; // An array from elsewhere
private static unsafe ulong WyHashCore(byte[] array)
{
var bVector = Vector256.Create(SOME_CONSTANT1, SOME_CONSTANT2, SOME_CONSTANT3, SOME_CONSTANT4);
fixed (byte* pData = array)
{
byte* ptr = pData;
// Consume 32-byte chunks
for (int i = 0; i < array.Length; i += 32)
{
ulong a = Read64(ptr, i);
ulong b = Read64(ptr, i + 8);
ulong c = Read64(ptr, i + 16);
ulong d = Read64(ptr, i + 24);
// Create a 256-bit vector from the 4 64-bit integers
var aVector = Vector256.Create(a, b, c, d);
// XOR the 2 vectors
var res = Avx2.Xor(aVector, bVector);
// Get the resulting values out of the result vector
ulong xor1 = res.GetElement(0);
ulong xor2 = res.GetElement(1);
ulong xor3 = res.GetElement(2);
ulong xor4 = res.GetElement(3);
// Use the resulting values
}
}
}
这确实给出了预期的结果 - 但它比仅乘以标量 慢 15 倍!
我是不是哪里出错了,还是误用了 SIMD?
** 更新 ** 我更新了代码以使用“正确”的方式向向量加载数据和从向量卸载数据,现在它比标量代码快大约 3.75 倍!
byte[] array; // An array from elsewhere
private static readonly Vector256<ulong> PrimeVector = Vector256.Create(SOME_CONSTANT1, SOME_CONSTANT2, SOME_CONSTANT3, SOME_CONSTANT4);
private static unsafe ulong WyHashCore(byte[] array)
{
// Create space on the stack to hold XOR results
var xorResult = stackalloc ulong[4];
fixed (byte* pData = array)
{
byte* ptr = pData;
// Consume 32-byte chunks
for (int i = 0; i < array.Length; i += 32)
{
// Create a 256-bit vector from the 4 64-bit integers
var vector = Avx.LoadVector256((ulong*)(ptr + i));
// XOR the 2 vectors
var res = Avx2.Xor(vector, PrimeVector);
// Store the resulting vector in memory
Avx2.Store(xorResult, res);
// Get the resulting values out of the result vector
var xor1 = *xorResult;
var xor2 = *(xorResult + 1);
var xor3 = *(xorResult + 2);
var xor4 = *(xorResult + 3);
// Use the resulting values
}
}
}
最佳答案
TL;DR AVX2 硬件内在函数使用不当导致生成非常低效的 SIMD 代码。
错误在于指令在缓冲区中加载、操作和存储数据的方式。该操作应使用 AVX/AVX2 Avx2.Xor 内在函数和内存执行,这将使加载时间加快 4 倍并返回 Vector256。另一方面,这会使对 Vector256.Create 的调用变得多余,并会进一步加快执行速度。最后,应使用 Avx2.Store() 内部函数将数据存储在数组中。这再次将代码加速大约 4 倍。
应该应用的最后一个优化是利用 CPU 指令级并行性。通常 SIMD 指令在预定义数量的 CPU 周期内执行,延迟可能大于 1 个 CPU 周期。这些参数是特定于 CPU 的,可以在以下位置找到:
- Intel 64 and IA-32 Architectures Software Developer Manuals ,
- Intel Intrinsics Guide
- Agner Fog's Instruction tables: Lists of instruction latencies, throughputs and micro-operation breakdowns for Intel, AMD and VIA CPUs
由于所有可以应用的优化都非常复杂,我稍后会在更长的文章中解释它们,但总的来说,与您正在处理的问题的基本情况相比,由于矢量化,我希望加速高达 4 倍。
您正在使用的代码示例是一个简单的循环,以 quad unsigned quadword 步长修改数据,非常适合通过优化编译器进行自动矢量化。当 GCC 9.1 使用选项 -O3 -march=haswell 优化相同的 C++ 循环时,生成的机器代码显示应用于循环的所有标准优化:
#include <cstdint>
void hash(uint64_t* buffer, uint64_t length) {
uint64_t* pBuffer = buffer;
const uint64_t CONST1 = 0x6753ul;
const uint64_t CONST2 = 0x7753ul;
const uint64_t CONST3 = 0x8753ul;
const uint64_t CONST4 = 0x9753ul;
for(uint64_t i = 0; i < length; i += 4)
{
*pBuffer ^= CONST1;
*(pBuffer + 1) ^= CONST2;
*(pBuffer + 2) ^= CONST3;
*(pBuffer + 3) ^= CONST4;
}
}
Godbolt Compiler Explorer result GCC 9.1
test rsi, rsi
je .L11
cmp rsi, -4
ja .L6
lea rdx, [rsi-1]
vmovdqa ymm1, YMMWORD PTR .LC0[rip]
xor eax, eax
shr rdx, 2
inc rdx
.L5:
vpxor ymm0, ymm1, YMMWORD PTR [rdi]
inc rax
add rdi, 32
vmovdqu YMMWORD PTR [rdi-32], ymm0
cmp rax, rdx
jb .L5
vzeroupper
.L11:
ret
.L6:
vmovdqa ymm1, YMMWORD PTR .LC0[rip]
xor eax, eax
.L3:
vpxor ymm0, ymm1, YMMWORD PTR [rdi]
add rax, 4
add rdi, 32
vmovdqu YMMWORD PTR [rdi-32], ymm0
cmp rsi, rax
ja .L3
vzeroupper
jmp .L11
.LC0:
.quad 26451
.quad 30547
.quad 34643
.quad 38739
Godbolt Compiler Explorer result Clang 8.0
.LCPI0_0:
.quad 26451 # 0x6753
.quad 30547 # 0x7753
.quad 34643 # 0x8753
.quad 38739 # 0x9753
hash(unsigned long*, unsigned long): # @hash(unsigned long*, unsigned long)
test rsi, rsi
je .LBB0_3
xor eax, eax
vmovaps ymm0, ymmword ptr [rip + .LCPI0_0] # ymm0 = [26451,30547,34643,38739]
.LBB0_2: # =>This Inner Loop Header: Depth=1
vxorps ymm1, ymm0, ymmword ptr [rdi + 8*rax]
vmovups ymmword ptr [rdi + 8*rax], ymm1
add rax, 4
cmp rax, rsi
jb .LBB0_2
.LBB0_3:
vzeroupper
ret
关于c# - AVX2 SIMD XOR 在 .NET 中没有产生性能改进,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56036938/