AVX 256 位代码的性能略低于等效的 128 位 SSSE3 代码
AVX 256-bit code performing slightly worse than equivalent 128-bit SSSE3 code
我正在尝试编写非常高效的汉明距离代码。受到 Wojciech Muła 的 极其聪明的 SSE3 popcount implementation 的启发,我编写了一个 AVX2 等效解决方案,这次使用 256 位寄存器。 我预计基于所涉及操作的双倍并行性至少有 30%-40% 的改进,但令我惊讶的是,AVX2 代码有点慢(大约 2%)!
有人能告诉我我没有获得预期性能提升的可能原因吗?
展开,两个 64 字节块的 SSE3 汉明距离:
INT32 SSE_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m128i paccum = _mm_setzero_si128();
__m128i a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA));
__m128i b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB));
__m128i err = _mm_xor_si128 (a, b);
__m128i lo = _mm_and_si128 (err, low_mask);
__m128i hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
__m128i popcnt1 = _mm_shuffle_epi8(lookup, lo);
__m128i popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 4));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 4));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 8));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 8));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 12));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 12));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
paccum = _mm_sad_epu8(paccum, _mm_setzero_si128());
UINT64 result = paccum.m128i_u64[0] + paccum.m128i_u64[1];
return (INT32)result;
}
使用 AVX 的 256 位寄存器的未展开等效版本:
INT32 AVX_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m256i paccum = _mm256_setzero_si256();
__m256i a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA));
__m256i b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB));
__m256i err = _mm256_xor_si256 (a, b);
__m256i lo = _mm256_and_si256 (err, low_mask256);
__m256i hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
__m256i popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
__m256i popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA + 8));
b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB + 8));
err = _mm256_xor_si256 (a, b);
lo = _mm256_and_si256 (err, low_mask256);
hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
paccum = _mm256_sad_epu8(paccum, _mm256_setzero_si256());
UINT64 result = paccum.m256i_i64[0] + paccum.m256i_u64[1] + paccum.m256i_i64[2] + paccum.m256i_i64[3];
return (INT32)result;
}
我已经验证了编译器发出的输出汇编代码,它看起来不错,可以将内部指令直接转换为机器指令。我唯一注意到的是,在 AVX2 版本上,最后一行累积 4 个四字的人口数,它生成比 SSE3 版本更复杂的代码(其中只需要累积 2 个四字就可以得到人口数量),但我仍然希望吞吐量更快。
为四字累加生成的 AVX2 代码
vextractf128 xmm0, ymm2, 1
psrldq xmm0, 8
movd ecx, xmm2
movd eax, xmm0
vextractf128 xmm0, ymm2, 1
psrldq xmm2, 8
add eax, ecx
movd ecx, xmm0
add eax, ecx
movd ecx, xmm2
add eax, ecx
为四字累加生成的 SSE3 代码
movd ecx, xmm2
psrldq xmm2, 8
movd eax, xmm2
add eax, ecx
我的测试程序每个例程调用 100 万次,使用不同的输入值,但重复使用两个静态缓冲区来保存 pA
和 pB
参数的数据。在我对 CPU 体系结构的有限理解中,这个位置(一遍又一遍地重复使用相同的内存缓冲区)应该很好地预热 CPU 缓存并且不会被内存带宽问题所束缚,但除了可能的内存之外带宽,我不明白为什么没有性能提升。
测试例程
int _tmain(int argc, _TCHAR* argv[]) {
lookup = _mm_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask = _mm_set1_epi8(0xf);
lookup256 = _mm256_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4,
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask256 = _mm256_set1_epi8(0xf);
std::default_random_engine generator;
generator.seed(37);
std::uniform_int_distribution<UINT32> distribution(0, ULONG_MAX);
auto dice = std::bind( distribution, generator);
UINT32 a[16];
UINT32 b[16];
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice();
b[j] = dice();
}
count+= AVX_PopCount(a, b);
}
}
cout << count << "\r\n";
std::default_random_engine generator2;
generator2.seed(37);
std::uniform_int_distribution<UINT32> distribution2(0, ULONG_MAX);
auto dice2 = std::bind( distribution2, generator2);
count = 0;
{
cout << "SSE PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice2();
b[j] = dice2();
}
count+= SSE_PopCount(a, b);
}
}
cout << count << "\r\n";
getch();
return 0;
}
测试机是Intel Corei7 4790,我用的是Visual Studio 2012 Pro
除了注释中的小问题(为 /arch:AVX
编译)之外,您的主要问题是每次迭代时随机输入数组的生成。这是你的瓶颈,所以你的测试没有有效地评估你的方法。注意 - 我没有使用 boost,但 GetTickCount
可用于此目的。仅考虑:
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
AVX PopCount
2309
256002470
所以需要 2309 毫秒才能完成...但是如果我们完全摆脱您的 AVX 例程会怎样?只需制作输入数组:
int count;
count = 0;
{
cout << "Just making arrays...\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
Just making arrays...
2246
怎么样。这并不奇怪,真的,因为你要生成 32 个随机数,这可能非常昂贵,然后只执行一些相当快的整数数学和洗牌。
所以...
现在让我们再增加 100 次迭代,使随机生成器脱离紧密循环。在禁用优化的情况下编译此处将 运行 您的代码按预期进行并且不会丢弃 "useless" 迭代 - 大概我们在这里关心的代码已经(手动)优化了!
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
count = 0;
{
cout << "SSE PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += SSE_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
产生输出:
AVX PopCount
3744
730196224
SSE PopCount
5616
730196224
那么恭喜 - 您可以拍拍自己的背,您的 AVX 例程确实比 SSE 例程快三分之一(此处在 Haswell i7 上测试)。这个教训是要确保你实际上是在分析你认为你在分析的东西!
您应该考虑使用通常的 _mm_popcnt_u64
指令,而不是在 SSE 或 AVX 中破解它。我彻底测试了所有 popcounting 方法,包括 SSE 和 AVX 版本(最终导致我或多或少出名 question about popcount)。 _mm_popcnt_u64
大大优于 SSE 和 AVX,尤其是当您使用的编译器可以防止我的问题中发现的 Intel popcount 错误时。没有这个错误,我的 Haswell 能够弹出计数 26 GB/s,这几乎达到了总线带宽。
_mm_popcnt_u64
更快的原因仅仅是因为它一次弹出 64 位(因此已经是 AVX 版本的 1/4),同时只需要一个廉价的处理器指令。它只花费几个周期(英特尔的延迟为 3,吞吐量为 1)。即使您使用的每条 AVX 指令只需要一个周期,由于弹出计数 256 位所需的指令数量过少,您仍然会得到更糟糕的结果。
试试这个,应该是最快的:
int popcount256(const uint64_t* u){
return _mm_popcnt_u64(u[0]);
+ _mm_popcnt_u64(u[1]);
+ _mm_popcnt_u64(u[2]);
+ _mm_popcnt_u64(u[3]);
}
我知道这不能回答您为什么 AVX 较慢的核心问题,但由于您的最终目标是快速 popcount,因此 AVX <-> SSE 比较无关紧要,因为两者都不如内置 popcount。
我正在尝试编写非常高效的汉明距离代码。受到 Wojciech Muła 的 极其聪明的 SSE3 popcount implementation 的启发,我编写了一个 AVX2 等效解决方案,这次使用 256 位寄存器。 我预计基于所涉及操作的双倍并行性至少有 30%-40% 的改进,但令我惊讶的是,AVX2 代码有点慢(大约 2%)!
有人能告诉我我没有获得预期性能提升的可能原因吗?
展开,两个 64 字节块的 SSE3 汉明距离:
INT32 SSE_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m128i paccum = _mm_setzero_si128();
__m128i a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA));
__m128i b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB));
__m128i err = _mm_xor_si128 (a, b);
__m128i lo = _mm_and_si128 (err, low_mask);
__m128i hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
__m128i popcnt1 = _mm_shuffle_epi8(lookup, lo);
__m128i popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 4));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 4));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 8));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 8));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 12));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 12));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
paccum = _mm_sad_epu8(paccum, _mm_setzero_si128());
UINT64 result = paccum.m128i_u64[0] + paccum.m128i_u64[1];
return (INT32)result;
}
使用 AVX 的 256 位寄存器的未展开等效版本:
INT32 AVX_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m256i paccum = _mm256_setzero_si256();
__m256i a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA));
__m256i b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB));
__m256i err = _mm256_xor_si256 (a, b);
__m256i lo = _mm256_and_si256 (err, low_mask256);
__m256i hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
__m256i popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
__m256i popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA + 8));
b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB + 8));
err = _mm256_xor_si256 (a, b);
lo = _mm256_and_si256 (err, low_mask256);
hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
paccum = _mm256_sad_epu8(paccum, _mm256_setzero_si256());
UINT64 result = paccum.m256i_i64[0] + paccum.m256i_u64[1] + paccum.m256i_i64[2] + paccum.m256i_i64[3];
return (INT32)result;
}
我已经验证了编译器发出的输出汇编代码,它看起来不错,可以将内部指令直接转换为机器指令。我唯一注意到的是,在 AVX2 版本上,最后一行累积 4 个四字的人口数,它生成比 SSE3 版本更复杂的代码(其中只需要累积 2 个四字就可以得到人口数量),但我仍然希望吞吐量更快。
为四字累加生成的 AVX2 代码
vextractf128 xmm0, ymm2, 1
psrldq xmm0, 8
movd ecx, xmm2
movd eax, xmm0
vextractf128 xmm0, ymm2, 1
psrldq xmm2, 8
add eax, ecx
movd ecx, xmm0
add eax, ecx
movd ecx, xmm2
add eax, ecx
为四字累加生成的 SSE3 代码
movd ecx, xmm2
psrldq xmm2, 8
movd eax, xmm2
add eax, ecx
我的测试程序每个例程调用 100 万次,使用不同的输入值,但重复使用两个静态缓冲区来保存 pA
和 pB
参数的数据。在我对 CPU 体系结构的有限理解中,这个位置(一遍又一遍地重复使用相同的内存缓冲区)应该很好地预热 CPU 缓存并且不会被内存带宽问题所束缚,但除了可能的内存之外带宽,我不明白为什么没有性能提升。
测试例程
int _tmain(int argc, _TCHAR* argv[]) {
lookup = _mm_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask = _mm_set1_epi8(0xf);
lookup256 = _mm256_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4,
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask256 = _mm256_set1_epi8(0xf);
std::default_random_engine generator;
generator.seed(37);
std::uniform_int_distribution<UINT32> distribution(0, ULONG_MAX);
auto dice = std::bind( distribution, generator);
UINT32 a[16];
UINT32 b[16];
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice();
b[j] = dice();
}
count+= AVX_PopCount(a, b);
}
}
cout << count << "\r\n";
std::default_random_engine generator2;
generator2.seed(37);
std::uniform_int_distribution<UINT32> distribution2(0, ULONG_MAX);
auto dice2 = std::bind( distribution2, generator2);
count = 0;
{
cout << "SSE PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice2();
b[j] = dice2();
}
count+= SSE_PopCount(a, b);
}
}
cout << count << "\r\n";
getch();
return 0;
}
测试机是Intel Corei7 4790,我用的是Visual Studio 2012 Pro
除了注释中的小问题(为 /arch:AVX
编译)之外,您的主要问题是每次迭代时随机输入数组的生成。这是你的瓶颈,所以你的测试没有有效地评估你的方法。注意 - 我没有使用 boost,但 GetTickCount
可用于此目的。仅考虑:
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
AVX PopCount
2309
256002470
所以需要 2309 毫秒才能完成...但是如果我们完全摆脱您的 AVX 例程会怎样?只需制作输入数组:
int count;
count = 0;
{
cout << "Just making arrays...\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 1000000; i++) {
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
产生输出:
Just making arrays...
2246
怎么样。这并不奇怪,真的,因为你要生成 32 个随机数,这可能非常昂贵,然后只执行一些相当快的整数数学和洗牌。
所以...
现在让我们再增加 100 次迭代,使随机生成器脱离紧密循环。在禁用优化的情况下编译此处将 运行 您的代码按预期进行并且不会丢弃 "useless" 迭代 - 大概我们在这里关心的代码已经(手动)优化了!
for (int j = 0; j < 16; j++) {
a[j] = dice();
b[j] = dice();
}
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += AVX_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
count = 0;
{
cout << "SSE PopCount\r\n";
unsigned int Tick = GetTickCount();
for (int i = 0; i < 100000000; i++) {
count += SSE_PopCount(a, b);
}
Tick = GetTickCount() - Tick;
cout << Tick << "\r\n";
}
cout << count << "\r\n";
产生输出:
AVX PopCount
3744
730196224
SSE PopCount
5616
730196224
那么恭喜 - 您可以拍拍自己的背,您的 AVX 例程确实比 SSE 例程快三分之一(此处在 Haswell i7 上测试)。这个教训是要确保你实际上是在分析你认为你在分析的东西!
您应该考虑使用通常的 _mm_popcnt_u64
指令,而不是在 SSE 或 AVX 中破解它。我彻底测试了所有 popcounting 方法,包括 SSE 和 AVX 版本(最终导致我或多或少出名 question about popcount)。 _mm_popcnt_u64
大大优于 SSE 和 AVX,尤其是当您使用的编译器可以防止我的问题中发现的 Intel popcount 错误时。没有这个错误,我的 Haswell 能够弹出计数 26 GB/s,这几乎达到了总线带宽。
_mm_popcnt_u64
更快的原因仅仅是因为它一次弹出 64 位(因此已经是 AVX 版本的 1/4),同时只需要一个廉价的处理器指令。它只花费几个周期(英特尔的延迟为 3,吞吐量为 1)。即使您使用的每条 AVX 指令只需要一个周期,由于弹出计数 256 位所需的指令数量过少,您仍然会得到更糟糕的结果。
试试这个,应该是最快的:
int popcount256(const uint64_t* u){
return _mm_popcnt_u64(u[0]);
+ _mm_popcnt_u64(u[1]);
+ _mm_popcnt_u64(u[2]);
+ _mm_popcnt_u64(u[3]);
}
我知道这不能回答您为什么 AVX 较慢的核心问题,但由于您的最终目标是快速 popcount,因此 AVX <-> SSE 比较无关紧要,因为两者都不如内置 popcount。