我有一个32位的数字,想知道有多少位是1。
我正在考虑这个伪代码:
mov eax, [number]
while(eax != 0)
{
div eax, 2
if(edx == 1)
{
ecx++;
}
shr eax, 1
}
有更有效的方法吗?
我在x86处理器上使用NASM。
(我刚开始使用汇编程序,所以请不要告诉我使用extern库中的代码,因为我甚至不知道如何包含它们;)
(我刚刚发现How to count the number of set bits in a 32-bit integer?也包含我的解决方案。还有其他解决方案,但不幸的是我似乎无法弄明白,我将如何在汇编程序中编写它们)
答案 0 :(得分:7)
最有效的方法(无论如何,执行时间)是一个查找表。显然你不会有一个40亿的条目表,但你可以将32位分解为8位块,只需要一个256条目表,或者进一步分成4位块,只需要16个条目。祝你好运!
答案 1 :(得分:6)
在具有SSE4支持的处理器中,您可以使用POPCNT指令为您执行此操作。
最天真的算法实际上比你想象的要快(DIV指令真的很慢)。
mov eax, [number]
xor ecx,ecx
loop_start:
test eax,1
jnz next
inc ecx
next:
shr eax, 1
mov eax,ecx
关于您之前的SO答案的评论,我将从那里做一个示例答案,并指导我如何转换它。
long count_bits(long n) {
unsigned int c; // c accumulates the total bits set in v
for (c = 0; n; c++)
n &= n - 1; // clear the least significant bit set
return c;
}
(我假设你知道如何定义一个函数和有趣的东西)。 我们需要的是一个非常简单的循环,一个计数器变量(传统上,ecx是索引和计数器)和位测试指令。
mov edx,n
xor ecx,ecx
loop_start:
test edx,edx
jz end
mov ebx,edx
dec ebx
and edx,ebx
inc ecx
jmp loop_start
end:
mov eax,ecx
ret
在汇编中实现像汉明重量算法这样的东西并不复杂,但只是足够复杂,你宁愿不把它作为一个初步的作业问题。
答案 2 :(得分:4)
我的x86汇编程序有点生疏,但想到这一点:
clc ; clear carry
xor ecx, ecx ; clear ecx
shl eax, 1 ; shift off one bit into carry
adc ecx, 0 ; add carry flag to ecx
; ... repeat the last two opcodes 31 more times
ecx
包含您的位数。
x86 shift instructions将CF
设置为移出的最后一位,adc ecx, 0
将其读取。
答案 3 :(得分:2)
为了记录,如果您想要良好的性能,您通常希望避免循环/分支,使用 8 位表查找或乘法 bithack(GCC 的当前标量回退 __builtin_popcnt
而没有 {{1} })。如果您的数字通常很小(右移 1),或者如果您的数字通常只设置了几位(循环使用 -mpopcnt
清除设置的最低位),那么循环几乎没有问题。但是对于设置了一半或更多位的数字,这些表现相当糟糕。
大多数现代 x86 CPU 都支持 the popcnt instruction。它由 SSE4.2 暗示,但也有自己的 CPUID 功能位,因此 CPU 可以在没有 SSE4.2 的情况下拥有它。英特尔酷睿 2 及更早版本没有具有此功能。
x & (x-1)
如果您不介意覆盖同一个寄存器,例如 xor eax,eax ; avoid false dependency on Sandybridge-family before IceLake
popcnt eax, edi
可以避免输出错误依赖的危险:您已经对同一个寄存器产生了真正的依赖。 (Why does breaking the "output dependency" of LZCNT matter?)
如果没有 HW popcnt edi, edi
,另一种选择是 SSSE3 popcnt
,它实际上非常适合计算大型数组,特别是如果您有 AVX2。见
可以进行数组查找,使用 pshufb
/ movzx ecx, al
/ movzx edx, ah
等提取每个字节。然后是 shr eax, 16
/ movzx ecx, [table + rcx]
。请注意,总结果最多为 64,因此不会溢出 8 位寄存器。这将需要一个 256 字节的表来保持高速缓存中的热状态以获得良好的性能。如果您执行了 lot 的 popcnt 但不能使用 SIMD,这可能是一个不错的选择;针对您的用例对 bithack 进行基准测试。
如果在编译时未启用 HW popcnt,则来自 https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel / How to count the number of set bits in a 32-bit integer? 的小技巧是 GCC 当前使用的。 (即在 libgcc 辅助函数中)。有关 bithack 如何/为什么将位求和为 2 位累加器,然后再次水平为 4 位等的解释,请参阅该答案(有趣的事实:GCC 和 clang 实际上将 C 逻辑识别为 popcnt 习语并将其编译为带有 add cl, [table + rdx]
的 popcnt
指令。以下 asm 是 GCC -O3 output without -mpopcnt;我看不到任何手动改进它的方法。
它尽可能使用 EAX 作为 AND 的目标,以允许 -mpopcnt
短格式没有 modrm 字节。)
这个非分支代码,不需要任何数据查找,所以它不能缓存未命中(I-cache除外),如果你关心popcount性能(尤其是延迟)但不关心它可能很好经常这样做可以使查找表在缓存中保持热度。 (或者对于 64 位整数,64 位版本可能比 8x 字节查找更好。)
and eax, imm32
对于 64 位整数,它是相同的序列,以 64 位乘法结束。 (但您需要 ; x86-64 System V calling convention
; but also of course works for 32-bit mode with the arg in a register
numberOfSetBits: ; 32-bit unsigned int x in EDI
mov eax, edi
shr eax, 1
and eax, 0x55555555 ; (x>>1) & 0x55555555
sub edi, eax ; x -= ((x>>1) & 0x55555555) 2-bit sums
mov eax, edi
shr edi, 0x2
and eax, 0x33333333
and edi, 0x33333333
add edi, eax ; pairs of 2-bit accumulators -> 4
mov eax, edi
shr eax, 0x4
add eax, edi ; we can add before masking this time without overflow risk
and eax, 0x0f0f0f0f
imul eax, eax, 0x01010101 ; sum the 4 bytes into the high byte (because their values are small enough)
shr eax, 24
ret
来具体化 64 位掩码和乘数常量;它们不能作为 AND 或 IMUL 的立即数起作用。
像 RORX 这样的指令对于更有效地复制和移动而不是 mov/shr 可能很有用,但是任何带有 RORX 的 CPU 也会有 POPCNT,所以你应该使用它! LEA 复制和左移没有帮助:加法从低到高传播进位,因此为了避免在第一步中丢失顶部的位,您需要右移。 mov reg, imm64
步骤也无法添加到每对 2 位累加器中的较高者:该点的最大和为 >>2
,并且需要 3 位来表示,因此最高的累加器 (在寄存器的顶部)如果你做了 4
/ 2x 和 / add,可能会丢失一个计数,因为不是 4 位未对齐,它只有 2。你最终需要右移来放置计数器在 imul 之前的某个时间点回到其字节的底部,因此即使可以在早期步骤中使用左移/添加,您也会延长关键路径延迟。
对于较小的代码大小(但不是速度),Hamming weight ( number of 1 in a number) mixing C with assembly 中显示的循环非常好。其 NASM 版本如下所示:
lea eax, [rdi + rdi]
如果您输入中的设置位可能接近顶部,请使用 ;;; Good for small inputs (all set bits near the bottom)
;; input: EDI (zeroed when we're done)
;; output: EAX = popcnt(EDI)
popcount_shr_loop:
xor eax, eax
; optional: make the first adc non-redundant. Otherwise just fall into the loop (with CF=0 from xor)
shr edi, 1 ; shift low bit into CF
;; jz .done ; not worth running an extra instruction for every case to skip the loop body only for the input == 0 or 1 case
.loop:
adc eax, 0 ; add CF (0 or 1) to result
shr edi, 1
jnz .loop ; leave the loop after shifting out the last bit
;.done:
adc eax, 0 ; and add that last bit
ret
而不是 shl
。
对于更小的代码大小,您可以在进入循环之前跳过 shr
,并执行多余的 shr
。 (异或归零清除 CF)。
@spoulson's answer 建议展开循环 32 次(没有 jz .done)。当您想要一个大的直线代码块以使用任意位模式获得最大速度时,以乘法结尾的 bithack shift/and/add 会更好。 adc
在大多数 CPU 上是 1 uop,除了 Intel P6 系列(PPro 到 Nehalem)。 (adc reg,0
是 a special case on Intel SnB-family before Broadwell),但 64 uop 和 32 周期延迟与 15 uop bithack 相比仍然很差。
但是,将其展开 2 或 4 作为中间立场是有意义的。这将使不同的输入以相同的方式分支,例如每个设置位在低 4 位的输入都将通过循环一次,不采用分支。
0
您可以尝试通过执行 popcount_shr_loop_unroll2:
xor eax, eax
shr edi, 1 ; shift low bit into CF
;; jz .done ; still optional, but saves more work in the input <= 1 case. Still not worth it unless you expect that to be very common.
.loop:
%rep 2 ;; Unroll
adc eax, 0 ; add CF (0 or 1) to result
shr edi, 1
%endrep ;; still ending with ZF and CF set from a shift
jnz .loop ; leave the loop on EDI == 0
;.done:
adc eax, 0 ; there may still be a bit we haven't added yet
ret
/ shr edi, 4
作为循环分支来 let out-of-order exec see the loop-exit condition sooner,并让循环体将 EDI 复制到另一个寄存器,并在一次移出低 4 位 1时间。但那时你可能只想要 bithack 版本;具有 OoO exec 的 x86 CPU 也具有快速的 imul r32,例如 Pentium II/III 上的 4 个周期延迟,AMD K8 及更高版本上的 3 个周期延迟,以及自 Core 2 以来的 Intel。它们的代码获取/解码能力应该处理涉及 32 的更大指令-位掩码常量足够好。
(由于我们考虑的是旧 CPU:在 P5 Pentium 上,jnz
和 shr
都只能在 U 管中运行,因此展开不会让它们相互配对以利用ILP。)
另一个选项是在 adc
/ lea edx, [rdi-1]
上循环(清除最低设置位,如果 ZF 为零则设置 ZF)。这对于只有几个设置位的数字来说是可以的。
and edi, edx
有关 ;; could be good if very few bits are set, even if they're scattered around
;; Input: EDI (zeroed when done)
;; output: EAX = popcount(EDI)
;; clobbers: EDX
popcount_loop_lsr:
xor eax,eax
test edi,edi
jz .done ; if(!x) return 0;
.loop: ; do{
inc eax ; ++count
lea edx, [rdi-1]
and edi, edx ; x &= x-1 clear lowest set bit
jnz .loop ; }while(x)
.done:
ret
之类的更多 bithacks,请参阅 https://catonmat.net/low-level-bit-hacks。另请注意,BMI1 instruction blsr
会执行此操作,因此当您已经打开 x86 指令参考时,这是一个方便检查公式的地方。但当然,如果你有 BMI1,你就会有 x & (x-1)
。 popcnt 实际上有它自己的功能位,但没有任何现实世界的 CPU 有 BMI1 但没有 popcnt/SSE4.2。
请注意,这与通过 LEA 和 AND 的 2 周期循环相关性不同,与另一个循环中通过 SHR 和 ADC(假设为单 uop ADC)的 1 周期相关性不同。所以每次迭代都有两倍长的数据依赖。但从好的方面来说,我们只是遍历 set 位,跳过零。尽管如此,最坏的情况 (popcnt
) 的延迟是两倍。
EDI=-1
实际上可以将英特尔 SnB 系列上的宏熔断器融合为单个和分支 uop。 (因为它就像 and/jnz
)。所以每次迭代仍然只有 3 个前端 uops,但不太可能很快检测到分支错误预测,因此就整体前端成本而言,这个版本可能很糟糕。
由于 test
只是计算循环迭代,对 inc eax
更新逻辑没有数据依赖,展开仍然需要一个分支,我认为,除非你在循环后做了一些额外的逻辑来检查是否中间临时已经为零。由于 x
dep 链是关键路径,展开可能没有帮助。
答案 4 :(得分:1)
mov eax,[c]
xor ebx,ebx
SSS: shr eax,1 ; after shift, if eax=0 ZF flag=1
jz XXX ; end (no more bit on eax)
adc bl
jmp SSS
XXX: adc bl
movb [Nbit],bl
答案 5 :(得分:0)
此程序为您提供32位数字中的1的数字。试试:)
extern printf
SECTION .data
msg: db "The number of 1 bits are: %d",10,0
inta1: dd 1234567
num: dd 2147483647
SECTION .text
global main
main:
mov eax, [num]
mov ecx,32
mov edx,0
.loop: dec ecx
cmp ecx,0
jl .exit
shr eax,1
jnc .loop
inc edx
jmp .loop
.exit:
push edx
push dword msg
call printf
add esp, 8
答案 6 :(得分:0)
使用bsf(位扫描前移)可能比普通移位更有效。
xor edx,edx
mov eax,num
bsf ecx,eax
je end_bit_count
; align?
loop_bit_count:
inc ecx
inc edx
shr eax,cl
bsf ecx,eax
jne loop_bit_count
end_bit_count:
答案 7 :(得分:-3)
最好的方法:
tabx:array [0..255] of byte = //number of bit for each byte (COPY THIS TABLE)
(0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,
1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8);
In MASM:
asm
mov eax,number //32 bit
movzx ecx,tabx[al] //for clear ecx except cl
addb cl,tabx[ah] //add ah to cl
shr eax,16 //put left part in ah-al
addb cl,tabx[al]
addb cl,tabx[ah]
mov result,ecx