Bit popcount for large buffer, with Core 2 CPU (SSSE3)
我正在寻找在512字节或更多字节的大缓冲区上进行popcount的最快方法。我可以保证任何所需的对齐,并且缓冲区大小总是2的幂。缓冲区与块分配相对应,因此通常情况下,位要么全部设置,要么全部不设置,要么大部分设置为偏向缓冲区的"左",偶尔会有孔。
我考虑过的一些解决方案是:
- GCC的__builtin_popcount。
- 位片popcount_24words。
- 数位集,布莱恩·克尼根的方法
我对最快的解决方案感兴趣,它必须在32位x86芯片组上工作,属于core2或更高版本。SSE和SIMD非常感兴趣。我将在以下四核CPU上进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| matt@stanley:~/anacrolix/public/stackoverflow$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 15
model name : Intel(R) Core(TM)2 Quad CPU Q6600 @ 2.40GHz
stepping : 11
cpu MHz : 1600.000
cache size : 4096 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 4
apicid : 0
initial apicid : 0
fdiv_bug : no
hlt_bug : no
f00f_bug : no
coma_bug : no
fpu : yes
fpu_exception : yes
cpuid level : 10
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe nx lm constant_tsc arch_perfmon pebs bts aperfmperf pni dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm lahf_lm tpr_shadow vnmi flexpriority
bogomips : 4800.21
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management: |
- SSE4有POCPNT
- @AAA鲤鱼:请提供一个代码示例,并以此作为答案!链接到popcnt的规范描述以及如何在gcc上使用它也是一个好主意。
- @马特,你发现这里提到了secure.wikimedia.org/wikipedia/en/wiki/sse4
- @Jens Gustedt:我知道这条指令(尽管我的CPU不支持它),但不知道它在gcc上的用法。
- @马特:如果您使用gcc,在任何情况下我都不会担心在汇编程序中实现这一点。我相信他们,使用__builtin_popcountll并使用-march=native编译。但是我的机器上也没有这个指令,所以我不能确认这是在做正确的事情:在我的机器上,这仍然会导致函数调用。
- 为什么?谷歌第一次点击"popcount"似乎是Bart Massey(XCB的作者)最近的一页,记录了他对最佳popcount算法的搜索,其中不仅包括他尝试的算法,还包括他的基准代码和结果。
- 您上面显示的CPU无论如何都没有popcnt指令(此指令的存在有一个特定的功能标志,在/proc/cpuinfo的flags行中显示为popcnt)。
- @马修·斯莱特:是的,我已经指出了,我希望sse4能接受Popcnt指令。
- @是的,我已经看过了,并且通过了这些,它们没有针对大型缓冲区进行优化。
- @马特:也许我遗漏了一些东西,但是什么(除了可能的simd指令)会导致单个单词的最有效算法不是大缓冲区的最有效算法?
- @llasram:一些例子:24个单词链接我的问题能够在96个字节的块上操作而不需要一个分支。加快对单个单词的操作是很好的,但是对于大型数组,隐式边界检查等仍然有O(N)成本。另一个是展开,针对大型缓冲区优化的算法可以将其应用到巨大的效果中。通常,组合一个非平凡的指令序列可以在远小于单独操作单词的周期内执行popcount(或其他一些任务)。我发现的另一个算法设法使用mult指令来消除循环。
- 原子性/多线程访问有什么要求吗?
- @荣:我不明白。
- @马特:我希望popcnt可以在很小的周期内实现,如果是这样的话,它可能很难被击败,特别是如果你展开一个包含popcnt 16x的循环或者类似的循环。如果没有popcnt,那么可能会应用复杂的程序集代码。
- [抱歉,这么老的问题]虽然这样的实验总是很有趣,有时甚至有用,但我想指出(毫无理智,显而易见的原因)我只是在我最近的(Skylake)桌面上编译并运行了测试套件。不出所料,使用编译器内部函数的最简单、最直接、最可读的解决方案比"最佳"优化(完全不可读)版本快4倍以上。
参见AMD软件优化指南第195页中的32位版本以了解一个实现。这将直接为x86提供程序集代码。
看看斯坦福比特旋转木马的变种。斯坦福大学的版本对我来说是最好的。它看起来很容易编码为x86 ASM。
这些都不使用分支指令。
这些可以概括为64位版本。
对于32位或64位版本,您可以考虑执行SIMD版本。SSE2将执行4个双字或两个四字(双向128位)马上。您要做的是实现32的popcount或2个或4个可用寄存器中的64位。最后在xmm寄存器中有2到4组popcount完成后,最后一步是存储和添加PopCounts一起得到最终答案。猜测,我希望你做得更好,4平行32位PopCounts而不是2个并行的64位PopCounts,因为后者可能会接受1或2个附加指令在每次迭代中,它很容易将4、32位的值加在一起结束。
- 你能提供AMD指南的链接,以及你将使用的SIMD指令吗?
- @马特:哎呀,我把AMD优化指南链接弄砸了,修好了。
- @关于SIMD指令:代码示例主要由LOAD和SHIFT、ADD标量机器指令组成,没有分支。x86 SIMD指令很容易找到等价的指令;唯一真正的问题是确保操作数在128或256位边界上对齐,这取决于您使用的是哪个SIMD集。你说这对你来说不是问题。
- @马特:你让我玩这个……这里列出的所有解决方案都计算单个单词的popcnt。很明显,你可以展开一个循环来做多个单词,然后你可以进入simd。可能不太明显的是,我们可以一次计算几个单词的popcnt,就像计算一个单词所需指令的一半,然后进行摊销(我在so:-的空白处画了一个存在证明,但是它太大了,不适合这个评论。这将很好地与您的512字节缓冲区进行处理。你想要最快的答案吗?
- @艾拉·巴克斯特:我很高兴有人知道有什么不同。我很想找到一个比我现在拥有的更好的答案,或者为了简单和所需的芯片组功能而牺牲性能。
- @马特:如果你想要高性能的话,你就不会得到简单;你会得到一段复杂的代码,它利用了许多模糊的事实。想展示一下你到目前为止所拥有的东西吗?
我概述了我为下面的大型缓冲区的人口计数/汉明重量找到的最佳C/assembly函数。
最快的装配功能是ssse3_popcount3,这里描述。它需要SSSE3,可在英特尔酷睿2及更高版本上使用,AMD芯片组将于2011年推出。它使用SIMD指令以16字节块的形式进行popcount,并一次展开4个循环迭代。
最快的C函数是popcount_24words,在这里描述。它使用位切片算法。值得注意的是,我发现clang实际上可以生成适当的向量装配指令,从而显著提高了性能。除此之外,算法仍然非常快速。
- 还要注意,SSE4有一个POPCNT指令。
- @保罗:是的,这已经被提到了。我把问题的解限制在ssse3或更低,也发现popcnt虽然非常快速和方便,但并不比一些矢量化的解快。
- 这是不正确的。POPCNT是最快的方法。这里有基准和详细的解释。
- 这个答案可以通过显示实际代码而不是依赖于外部站点来大大改进。事实上,对您控制之外的资源所做的更改可能会使答案无效。
- @丹:在现代英特尔(哈斯韦尔和后来的公司)上,avx2击败了标量POPCNT。但是只有256位向量:avx1/ssse3不会更快,iirc。Ryzen很有趣;它每时钟有4个64位的POPCNT,所以它在标量方面可能做得最好。avx512 vpternlogd支持更多优化:使用按位和popcount的大型(0,1)矩阵乘法,而不是实际的int或float乘法?,特别是github.com/wojciechmula/sse popcount/blob/master/…对16x zmm向量(16x 512位)执行30x vpternlogd+1 vector popcnt。
如果你有popcnt:
http://kent-vandervelden.blogspot.com/2009/10/counting-bits-population-count-and.html
http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse42_ata.htm
我建议从Hacker'sDelight中实现一个优化的32位popcnt例程,但是在SSE向量中为4 x 32位整数元素实现它。然后,您可以在每次迭代中处理128位,与优化的32位标量例程相比,这将给您大约4倍的吞吐量。
- 我不知道它是如何在您所指的例程中实现的,但肯定的是,SSE4.1Popcnt指令只适用于一般用途,而不适用于XMM寄存器。
- @网络钓鱼网站:事实上-我不是建议使用SSE4.1Popcnt-黑客们喜欢C中的32位Popcnt有一些有效的程序,它可以作为SSE4x32位Popcnt实现的基础。然后,您将迭代您的数据块,一次128位,生成4 x 32位部分填充计数,然后可以在末尾求和以获得总填充计数。
- 实际上,我发现的一些最佳解决方案使用向量运算来进行128位计数。