给定std::bitset<64>位
,设置任意位数和位位置x
(0-63)
对位置X或更低的位进行计数或在X处的位未设置时返回0的最有效的方法是什么
注意:如果设置了该位,则返回值将始终至少为1
int countupto(std::bitset<64> bits, int X)
{
if (!bits[X]) return 0;
int total=1;
for (int i=0; i < X; ++i)
{
total+=bits[i];
}
return total;
}
注意:这不是一个如何计算32位整数中设置位数的dup吗?因为它询问的是0到X范围以外的所有位
这个C++让G++发出非常好的x86 ASM(godbolt编译器资源管理器)。我希望它在其他64位体系结构上也能高效地编译(如果std::bitset::count
有一个HW popcount,否则它总是很慢的部分;例如,一定要使用g++-march=nehalem
或更高的版本,或者如果您不想启用任何其他功能,如果您可以将代码限制为只在支持该x86指令的CPU上运行,则使用-mpopcnt
):
#include <bitset>
int popcount_subset(std::bitset<64> A, int pos) {
int high_bits_to_eliminate = 63 - pos;
A <<= (high_bits_to_eliminate & 63); // puts A[pos] at A[63].
return (A[63]? ~0ULL : 0) & A.count(); // most efficient way: great code with gcc and clang
// see the godbolt link for some #ifdefs with other ways to do the check, like
// return A[BSET_SIZE-1] ? A.count() : 0;
}
这在32位架构上可能不是最优的,因此,如果需要构建32位架构,请比较其他替代方案。
这将适用于其他大小的位集,只要您对硬编码的63
做一些操作,并将移位计数的&63
掩码更改为更通用的范围检查。为了在奇怪大小的位集上获得最佳性能,可以为目标计算机的size<=register widthes
创建一个具有专门化的模板函数。在这种情况下,将位集提取为适当宽度的unsigned
类型,并移到寄存器的顶部,而不是位集的顶部。
您希望这也能为位集<32>
生成理想的代码,但事实并非如此。GCC/CLANG仍然使用X86-64上64位寄存器。
对于较大的位集,移动整个内容将比仅仅在包含pos
的单词下方弹出单词,并在该单词上使用这个更慢。(如果您可以假设SSSE3而不是popcnt
insn硬件支持,或者对于32bit目标,这就是向量化popcount在x86上真正发挥作用的地方。AVX2 256bitpshufb
是执行批量popcount的最快方法,但如果没有AVX2,我认为64bitpopcnt
与128 bitpshufb
实现相当接近。请参阅注释了解更多讨论。)
如果您有一个由64位元素组成的数组,并且希望分别计算每个元素中某个位置以下的位,那么您肯定应该使用SIMD。该算法的移位部分向量化,而不仅仅是popcnt部分。对全零寄存器使用psadbw
对基于pshufb
的popcnt后的64位块中的字节进行水平求和,该popcnt分别为每个字节中的位产生计数。SSE/AVX没有64位算术右移,但您可以使用不同的技术来混合每个元素的高位。
您希望编译器输出的asm指令将:
这还有一个有趣的副作用,就是将要测试的位作为寄存器的顶部位。测试符号位,而不是任何其他任意位,所需的指令略少。算术右移可以将符号位广播到寄存器的其余部分,从而实现比通常更高效的无分支代码。
做popcount是一个讨论得很多的问题,但实际上是这个难题中比较棘手的部分。在x86上,有非常有效的硬件支持,但只有在最近的-足够的硬件上。在Intel CPU上,popcnt
指令仅在Nehalem和更新版本上可用。我忘了AMD是什么时候加入支持的。
因此,为了安全地使用它,您需要使用不使用popcnt
的回退来执行CPU调度。或者,制作独立的二进制文件,这些二进制文件依赖于/不依赖于某些CPU特性。
该广播可以通过63的算术右移来完成,该右移在高位的副本中移动。
clang从原始版本生成了此代码。在Glenn关于4的不同实现的一些催促之后,我意识到,通过编写更像我想要的ASM的源代码,我可以将gcc引向Clang的最佳解决方案。明显的((int64_t)something)>>63
来更直接地请求算术右移将不是严格可移植的,因为有符号右移是实现定义为算术或逻辑的。该标准没有提供任何可移植的算术右移运算符。(不过,这不是没有定义的行为。)不管怎样,幸运的是编译器足够聪明:只要给它足够的提示,gcc就会看到最好的方法。
这个源代码使用gcc和clang在x86-64和ARM64上编写了很好的代码。两者都只是在popcnt的输入上使用算术右移位(因此移位可以与popcnt并行运行)。它还可以在32bit x86上使用gcc进行很好的编译,因为掩蔽只发生在32bit变量上(在添加多个popcnt结果之后)。在32bit上(当位集大于寄存器时),函数的其余部分是糟糕的。
带有gcc的原始三元运算符版本
使用gcc 5.3.0-o3-march=nehalem-mtune=haswell
编译(较旧的gcc,如4.9.2,也仍然发出此命令):
; the original ternary-operator version. See below for the optimal version we can coax gcc into emitting.
popcount_subset(std::bitset<64ul>, int):
; input bitset in rdi, input count in esi (SysV ABI)
mov ecx, esi ; x86 variable-count shift requires the count in cl
xor edx, edx ; edx=0
xor eax, eax ; gcc's workaround for popcnt's false dependency on the old value of dest, on Intel
not ecx ; two's complement bithack for 63-pos (in the low bits of the register)
sal rdi, cl ; rdi << ((63-pos) & 63); same insn as shl (arithmetic == logical left shift)
popcnt rdx, rdi
test rdi, rdi ; sets SF if the high bit is set.
cmovs rax, rdx ; conditional-move on the sign flag
ret
看看如何证明C语句-x、~x+1和~(x-1)得到相同的结果?有关GCC使用-x==~x+1
二的补码标识的背景信息。(如果只想要结果的低部分,那么哪一个2的补码整数运算可以在不将输入中的高位清零的情况下使用呢?其中切线地提到shl
屏蔽了移位计数,所以我们只需要ecx
的低6位来保存63-pos
。主要是因为我最近写了这篇文章,任何还在读这段话的人可能会觉得很有趣。)
内联时,这些指令中的一些将会消失。(例如,gcc首先会在ecx中生成计数。)
使用Glenn的乘法而不是三元运算符思想(由use_mul
启用),gcc可以
shr rdi, 63
imul eax, edi
而不是xor
/test
/cmovs
。
MOV r,r
:1融合域uop,0延迟,无执行单元异或
-归零:1个融合域uop,无执行单元非
:P0/P1/P5/P6的1个uop,1C延迟,每0.25C吞吐量1个shl
(又名sal
),计数在cl
:P0/P6:2C延迟为3个UOP,每2C吞吐量为1个。(Agner Fog的数据表明IvyBridge对此只使用了2个UOP,这很奇怪。)POPCNT
:1个uop用于p1,3C延迟,每1C吞吐量1个SHR r,IMM
:1 uop用于P0/P6,1C潜伏期。每0.5C吞吐量为1个。IMUL r,r
:1UOP用于p1,3C延迟。ret
总数:
cmov(三元算子)版本:11个融合域UOP(前端:每2.75C一个)。执行单元:移位端口(P0/P6)仍然处于瓶颈状态,为每2c一个。延迟:从位集到结果的延迟为7c,从pos到结果的延迟为8c。(CMOV
为2C延迟,对于P0/P1/P5/P6中的任一项为2 UOP。)
Clang有一些不同的诀窍:它不是test
/cmovs
,而是通过使用算术右移将符号位广播到寄存器的所有位置来生成全1或全0的掩码。我喜欢它:使用和
而不是cmov
在Intel上效率更高。但是,它仍然具有数据依赖性,并且为分支的两边都做工作(这是cmov的主要缺点)。更新:有了正确的源代码,gcc也会使用这个方法。
clang 3.7-o3-wall-march=nehalem-mtune=haswell
popcount_subset(std::bitset<64ul>, int):
mov ecx, 63
sub ecx, esi ; larger code size, but faster on CPUs without mov-elimination
shl rdi, cl ; rdi << ((63-pos) & 63)
popcnt rax, rdi ; doesn't start a fresh dep chain before this, like gcc does
sar rdi, 63 ; broadcast the sign bit
and eax, edi ; eax = 0 or its previous value
ret
sar/和
替换了xor/test/cmov
,cmov
是Intel CPU上的2-UOP指令,所以这真的很好。(对于三元运算符版本)。
当使用multiply源版本或“bitbroadcast”源版本时,Clang仍然执行sar/和
技巧,而不是实际的imul
。所以这些帮助gcc而不伤害Clang。(SAR/和
肯定比SHR/IMUL
好:关键路径上的延迟少2C。)pow_of_two_sub
版本确实会造成碰撞(请参见第一个godbolt链接:为了避免出现杂乱的想法,本文省略了这个链接)。
MOV ecx,63
/子ecx,ESI
实际上在没有MOV消除的CPU上更快,用于reg,reg移动(零延迟和无执行端口,通过寄存器重命名处理)。这包括Intel之前的IvyBridge,但不包括最近的Intel和AMD CPU。
Clang的mov imm
/sub
方法只将pos
的一个延迟周期放入关键路径(超出位集->结果延迟周期),而不是将mov ecx、ESI
/not ecx
的两个延迟周期放入CPU上,其中mov r,r
具有1C延迟。
使用BMI2(Haswell和更高版本),最佳ASM版本可以将MOV
保存到ECX
。其他的工作原理都是一样的,因为shlx
像shl
一样,将其移位计数输入寄存器屏蔽到操作数大小。
x86 shift指令具有疯狂的CISC语义,如果shift计数为零,则标志不受影响。所以变量计数移位指令对标志的旧值有(潜在的)依赖关系。“normal”x86SHL r,CL
在Haswell上解码为3个UOP,但BMI2SHLX r,r,r
仅为1个。所以很糟糕的是,gcc仍然使用-march=haswell
发出sal
,而不是使用shlx
(它在其他一些情况下确实使用了这一点)。
// hand-tuned BMI2 version using the NOT trick and the bitbroadcast
popcount_subset(std::bitset<64ul>, int):
not esi ; The low 6 bits hold 63-pos. gcc's two-s complement trick
xor eax, eax ; break false dependency on Intel. maybe not needed when inlined.
shlx rdi, rdi, rsi ; rdi << ((63-pos) & 63)
popcnt rax, rdi
sar rdi, 63 ; broadcast the sign bit: rdi=0 or -1
and eax, edi ; eax = 0 or its previous value
ret
Intel Haswell的性能分析:6个融合域UOP(前端:每1.5C一个)。执行单位:2个P0/P6班UOP。1个p1 UOP。2任意端口UOP:(总执行端口限制中每1.25C有一个)。关键路径延迟:shlx
(1)->popcnt
(3)->和
(1)=5C位集->结果。(或来自位置
->结果的6c)。
请注意,内联时,人工(或智能编译器)可以避免使用异或eax,eax
。它之所以存在,是因为popcnt
对输出寄存器的错误依赖(在Intel上),并且我们需要eax
中的输出(调用方最近可能在长dep链中使用了它)。使用-mtune=bdver2
或其他方法时,gcc不会将用于popcnt
输出的寄存器归零。
内联时,我们可以使用至少在popcnt
的源reg之前就已经准备好的输出寄存器来避免这个问题。当以后不需要源代码时,编译器将执行就地的POPCNT rdi,rdi
,但这里不是这样。相反,我们可以选择在源之前已经准备好的另一个寄存器。popcnt
的输入依赖于63-pos
,我们可以对其进行重击,因此popcntRSI、RDI
对rsi的依赖不能延迟它。或者,如果我们在寄存器中有63
,我们可以POPCNT rsi,RDI
/SARX rax,rsi,REG_63
/和eax,ESI
。或者BMI2的3操作数移位指令也可以让我们在以后需要输入的情况下不重击输入。
这是如此轻量级,循环开销和设置输入操作数/存储结果将是主要因素。(63-pos
可以使用编译时常量进行优化,也可以在变量计数的任何地方进行优化。)
有趣的是,英特尔编译器搬起石头砸自己的脚,并没有利用A[63]是符号位的事实。SHL
/BT rdi,63
/JC
。它甚至以一种非常愚蠢的方式设置分支。它可以使eax归零,然后根据shl
设置的符号标志跳过popcnt或不跳过popcnt。
最佳分支实现,从Godbolt上-o3-march=corei7
的ICC13输出开始:
// hand-tuned, not compiler output
mov ecx, esi ; ICC uses neg/add/mov :/
not ecx
xor eax, eax ; breaks the false dep, or is the return value in the taken-branch case
shl rdi, cl
jns .bit_not_set
popcnt rax, rdi
.bit_not_set:
ret
这几乎是最优的:a[pos]==true
大小写有一个未取分支。但是,与无分支方法相比,它并没有节省很多。
如果a[pos]==false
情况更常见:跳过ret
指令,转到popcnt
/ret
。(或者在内联之后:跳转到执行popcnt
并跳回的末尾块)。
我需要得到一个32位数字中的1位数字,其中只有一个1位(总是)。在C++或ASM中最快的方法。 例如
问题内容: 考虑数组 我可以做 但这需要找到所有对象才可以找到第一个。 有没有更有效的方法? 我一直在试图找出我是否可以传递参数,从而获取的第一个分类,而不是最后一次。 编辑关于[dup]。 有几个原因使这个问题不同。 该问题和答案涉及价值观的平等。这是关于。 这些答案都遭受我的答案面临的同一问题。注意,我提供了一个完全有效的答案,但强调了它的效率低下。我正在寻找解决效率低下的问题。 编辑有关第二
问题内容: 除了通常的“保持计数器”方法以外,还有什么快速的方法可以计算BitSet中设置的位数? 问题答案: 该基数()方法返回集合的位的数目。
问题内容: 假设我们在集合中有一些项目,并且我们想使用某些比较器对它们进行排序,并期望结果在列表中: 一种方法是对列表中的项目进行排序,例如: Anothe方法正在使用排序流: 我想知道哪种方法更有效?排序流是否有任何优势(例如在多核上进行Faste排序)? 在运行时复杂性方面/最快方面是高效的。 我不相信自己要实现一个完美的基准,学习并不能真正启发我。 问题答案: 可以肯定地说,两种形式的排序都
问题内容: 和CSS 和有什么不一样?那你什么时候应该使用呢? 问题答案: CSS绝对定位 绝对定位是最容易理解的。您从CSS 属性开始: 这告诉浏览器应将要定位的所有内容从文档的正常流程中删除,并将其放置在页面上的确切位置。它不会影响HTML中它之前或之后的元素在网页上的放置方式,但是除非您重写它,否则 它将 取决于其父级的位置。 如果你想一个元素从文档窗口的顶部10个像素的位置,你会使用偏移与
我有一个多索引df,带有“海龟”列 我需要的是“网络Pos”。我想不出一个优雅的方法。我需要的是使用Numpy或熊猫的colNet位置。数据集很大,需要使用递归并避免崩溃。 6将被分为6次1 1 1 1 1 1 1 第一个1将乘以基本数量,因此为1*2 第二个1将与第一个1的结果乘以海龟系数2*3 第四个1将与第三个1计算的结果相乘,乘以18*3以上的海龟因子,以此类推,最后求和,得到第一行的结果