【发布时间】:2014-12-31 15:11:23
【问题描述】:
我有这个简单的二进制相关方法,它比 GCC 的 __builtin_popcount(我认为启用 SSE4 时映射到 popcnt 指令)比表查找和 Hakmem 位旋转方法好 x3-4 和 %25。
这里是大大简化的代码:
int correlation(uint64_t *v1, uint64_t *v2, int size64) {
__m128i* a = reinterpret_cast<__m128i*>(v1);
__m128i* b = reinterpret_cast<__m128i*>(v2);
int count = 0;
for (int j = 0; j < size64 / 2; ++j, ++a, ++b) {
union { __m128i s; uint64_t b[2]} x;
x.s = _mm_xor_si128(*a, *b);
count += _mm_popcnt_u64(x.b[0]) +_mm_popcnt_u64(x.b[1]);
}
return count;
}
我尝试展开循环,但我认为 GCC 已经自动执行此操作,因此我最终获得了相同的性能。您认为在不使代码过于复杂的情况下性能进一步提高了吗?假设 v1 和 v2 大小相同,大小是偶数。
我对它目前的表现很满意,但我只是想看看它是否可以进一步改进。
谢谢。
编辑:修复了 union 中的一个错误,结果证明这个错误使这个版本比内置的 __builtin_popcount 更快,无论如何我再次修改了代码,它再次比现在的内置稍快(15%)但我不认为值得为此投入时间。感谢所有 cmets 和建议。
for (int j = 0; j < size64 / 4; ++j, a+=2, b+=2) {
__m128i x0 = _mm_xor_si128(_mm_load_si128(a), _mm_load_si128(b));
count += _mm_popcnt_u64(_mm_extract_epi64(x0, 0))
+_mm_popcnt_u64(_mm_extract_epi64(x0, 1));
__m128i x1 = _mm_xor_si128(_mm_load_si128(a + 1), _mm_load_si128(b + 1));
count += _mm_popcnt_u64(_mm_extract_epi64(x1, 0))
+_mm_popcnt_u64(_mm_extract_epi64(x1, 1));
}
第二次编辑:原来内置是最快的,叹息。特别是使用 -funroll-loops 和 -fprefetch-loop-arrays 参数。像这样的:
for (int j = 0; j < size64; ++j) {
count += __builtin_popcountll(a[j] ^ b[j]);
}
第三次编辑:
这是一个有趣的 SSE3 并行 4 位查找算法。想法来自Wojciech Muła,实现来自 Marat Dukhan 的answer。感谢@Apriori 提醒我这个算法。下面是算法的核心,它非常聪明,基本上使用 SSE 寄存器作为 16 路查找表和低半字节作为选择表单元格的索引来计算字节的位。然后将计数相加。
static inline __m128i hamming128(__m128i a, __m128i b) {
static const __m128i popcount_mask = _mm_set1_epi8(0x0F);
static const __m128i popcount_table = _mm_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);
const __m128i x = _mm_xor_si128(a, b);
const __m128i pcnt0 = _mm_shuffle_epi8(popcount_table, _mm_and_si128(x, popcount_mask));
const __m128i pcnt1 = _mm_shuffle_epi8(popcount_table, _mm_and_si128(_mm_srli_epi16(x, 4), popcount_mask));
return _mm_add_epi8(pcnt0, pcnt1);
}
在我的测试中,这个版本是相当的;在较小的输入上稍快,在较大的输入上比使用 hw popcount 稍慢。我认为如果它在 AVX 中实现,它应该真的很出色。但我没有时间这样做,如果有人愿意的话,我很想听听他们的结果。
【问题讨论】:
-
这些可能也已经由编译器完成了。但是您可以预先计算
size64/2并将所有分配(j和x)移出循环。根据您要计算其位的变量的数量,您可以使用 SSE 寄存器而不是整数寄存器来使用(google for)popcount。与往常一样,您应该自己检查性能。 -
那个联合技巧可能会导致错误的代码。即使它没有暂时通过内存(但很可能会),它仍然会很糟糕。
-
@inetknght:谢谢,我只是在寻找想法。正如您所猜测的那样,这些优化是由编译器进行的,而取消联合实际上会损害性能。另外,我已经在使用 SSE 寄存器了。
-
@harold 我实际上尝试使用 __128i 变量、_mm_load_si128 加载值和 _mm_extract_epi64 访问寄存器的 64 位部分。那个版本结果更糟。
-
如果您愿意深入研究组装(您真的想在 上获得很多性能吗?),这似乎是一篇不错的文章:wm.ite.pl/articles/sse-popcount.html跨度>
标签: performance optimization x86 bit-manipulation sse