NASM:计算32位数中的多少位设置为1

时间:2010-05-28 18:20:00

标签: assembly x86 bit-manipulation nasm

我有一个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?也包含我的解决方案。还有其他解决方案,但不幸的是我似乎无法弄明白,我将如何在汇编程序中编写它们)

8 个答案:

答案 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 instructionsCF设置为移出的最后一位,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。见


使用基线 x86 指令的回退

可以进行数组查找,使用 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,0a 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 上,jnzshr 都只能在 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