【问题标题】:Need to count number of 1's in binary with assembly需要用汇编计算二进制中 1 的数量
【发布时间】:2019-01-15 17:16:48
【问题描述】:

我有一个任务,我应该计算我设置的具有奇数的二进制 1 的数量,然后我需要在 7 段显示器上显示它。

在代码上我写了一条我应该这样做的注释。

我正在使用德州仪器 msp430。我查看了其他解决方案,但他们使用 C 而不是组装,不幸的是无法弄清楚如何在组装时做到这一点。

       bis.b #11111111b, &P1DIR
       bic.b #11111111b, &P1OUT

loop_1:
       ; do stuff with &P1OUT
       call #delay
       ...

delay

       mov #0, R5
       mov #0, R4

odd_even:
           ;Over here i need to count number of 1's in binary but cant figure out how to do it
           jnz try
           jz delay_over


      ...
           ret

【问题讨论】:

  • 这是什么架构?请标记!

标签: assembly msp430


【解决方案1】:

有些算法对 8 位以上更好。 @rcgldr 的答案是 16 位或 32 位 popcount 的有用开始。请参阅How to count the number of set bits in a 32-bit integer? 了解一些 bithack 和其他算法,包括查表。

您可以考虑使用 4 位查找表。 MSP430 移位缓慢(每位 1 个周期,如果您没有 MSP430X,则每位 1 条指令)。或者使用一个大的 8 位查找表。

或者循环设置位,用v &= v - 1; 清除低位。在采用 MOV、DEC 和 AND 的 MSP430 中。如果通常只设置几个位,那就太好了,但它们通常是分散的。


但最简单和最小的代码大小方法是一次循环遍历所有位。

如果您要一次循环一位以保持其简单和紧凑,您希望通过转换为进位并使用 ADDC(add-with-carry)来利用进位标志。

我尝试编写 C 语言,编译器可以使用 ADDC 变成很好的 asm,但 https://godbolt.org/z/2Ev2IC 是我管理的最好的。 GCC 和 clang 对于 x86 和大多数其他架构的 tmp = a+a; carry = tmp<a; 习惯用法的 MSP430 表现不佳。

不管怎样,你首先想要的是 asm:

;; simple naive bit-count.  Small code-size and not too slow for 8 bits

;; input in r12,  result: r11 = popcount(r12)
mov.w     #0, r11        ; retval = 0
.popcount_loop:          ; do{
    add.b   r12,r12          ; shift a bit into carry flag
    addc    #0, r11          ; add that bit to r11:  r11 += 0 + C

    tst.b    r12
    jnz   .popcount_loop ; } while( (uint8_t)r12 != 0);

add 使用字节操作数大小意味着第 7 位进入 C,而不是第 15 位。

我们可以改为使用右移将低位放入 C 标志,特别是如果我们期望许多输入是小数字(因此非零位都朝向低位)结尾)。根据this copy of a MSP430 / MSP430X instruction-set referencegoogle发现,普通的MSP430没有右移功能,只能通过进位右移。 RRC[.W] / RRC.B。 MSP430X 有一些“旋转”,实际上是在零位上移动的,所以它们是真正的移动。但是如果我们在运行它之前确保 C=0,我们就不需要它。由于人口计数不会换行,ADDC 将为我们可靠地清除 C。

我们可以通过让 JNZ 和 ADDC 使用来自同一个 ADD 的标志来优化它以减少循环内循环的指令(相同的代码大小但运行速度更快)。由于 ADDC 也写入标志,这意味着它必须在下一次迭代中。所以我们必须扭曲循环。我们可以剥离第一次迭代并在循环外进行 ADD。之后我们不会检查零,但这很好。为 input = 0x80 运行一次额外的迭代不是正确性问题,也不值得花费额外的指令。

; simple looping popcount, optimized for small numbers (right shift)
; and optimized for fewer instructions inside the loop

;; input in r12,  result: r11 = popcount(r12)
xor.w     r11, r11        ; r11=0,  C=!Z=0.   (mov doesn't set flags; this saves a CLRC)

rrc.b     r12             ; C = lsb(r12);   r12 >>= 1  ; prep for first iter

.popcount_loop:            ; do{
    addc    #0, r11          ; result += C;  Clears C because r11 won't wrap
    rrc.b   r12              ; C = lsb(r12);   r12 >>= 1;  Z = (r12==0)
    jnz    .popcount_loop  ; } while( (uint8_t)r12 != 0);

    addc    #0, r11        ; we left the loop with the last bit still in C

如果您的输入值是零扩展的,您可以使用rrc.w r12,以便循环适用于 8 位或 16 位值。但它并没有变慢,因为它在将所有位向右移出后仍然退出。

倾斜循环并剥离第一次迭代的前半部分和最后一次迭代的后半部分,总共只花费了我们一个额外的指令。 (而且它们仍然都是单字指令。)


你提到奇数/偶数。 你真的只是想要平价吗? (人口数是奇数还是偶数)?这与所有位的水平 XOR 相同。

; Needs MSP430X for rrum, otherwise you can only shift by 1 bit per instruction

;; input in r12,  result: r12=parity(r12)
;; clobbers: r11
mov.b   r12, r11       ; copy the low byte, zero the upper byte of R11 (not that it matters)
rrum     #4, r11       ; costs 4 cycles for shift-count = 4
xor     r11, r12       ; low 4 bits ^= (high 4 bits >> 4)

mov.b   r12, r11
rrum     #2, r11       ; costs 2 cycles for shift-count = 2
xor     r11, r12       ; narrow again to 2 bits

mov.b   r12, r11
rrum    #1,  r11       ; costs 1 cycle for shift-count = 1.  
xor     r11, r12       ; narrow again to 2 bits

and      #1, r12       ; clear high garbage from the high bits.

; ret  if this isn't inline

您可以使用循环来执行此操作,例如使用 popcount 循环并在最后执行and #1, r12

我觉得如果我们向左移动(4 然后 2)并使用add.b r12,r12 执行最后一步(移动 1),也许我们可以保存指令,因为有符号溢出(V 标志)=carry_in XOR carry_out for the sign bit。在两个输入相同的情况下,现有的符号位将始终为 0+0=00 或 1+1=10,因此符号位 = 符号位的进位。

因此,对于像r12.b = XY?????? 这样的位模式,add.b r12,r12 设置V = X^Y,即输入前两位的水平异或。因为Y是MSB的进位,X是进位。

如果您想在其上进行分支,这会很好,但 MSP430 似乎没有设置或未设置在 V 上分支的 jXX。它有JLJGE(N XOR V) 上的分支(即有符号比较),但N 将等于MSB,所以N ^ V 只是C,在我们的左移V 设置V = N ^ C 之后.我想你必须从标志寄存器中取出标志字并移位/屏蔽它!或者测试那个标志位和 JNZ。

【讨论】:

    【解决方案2】:

    在大多数计算机上,没有硬件可以通过几条指令执行此操作。

    你要做的是一组掩码和班次:

    unsigned char to_count, nbr=0, mask=0x1, m;
    for (int i=0; i<8; i++) {
        m = to_count&mask ; //1 if LSB=1, 0 otherwise
        nbr += m;
        to_count >>=1 ;
    }
    

    对于更多位数,您可以采用更智能的策略来减少统计时间的计算时间,但对于 8 位,您将没有任何收益。

    【讨论】:

    • 表查找是一种折衷,您可以进行两次 4 位表查找,而它只花费您 16 个字节的表。尽管您仍然需要屏蔽、移位和计算表偏移量并进行读取,但您最终会得到多少代码,8 的循环可能仍然更便宜。
    • “大多数计算机”都有它,如果您指的是台式机/笔记本电脑/手机。自 Nehalem 和(某些)K10 以来的所有 x86 都有它。 ARMv7 有它(仅适用于 SIMD vcnt / `cnt),编译器认为将它用于 AArch64 是值得的 integer->NEON->integer,但不适用于 ARM32 (godbolt.org/z/DKjgie)。但是,是的,MSP430没有在硬件中拥有它。如果您将世界上所有的微控制器本身都算作“计算机”,那么肯定可能世界上超过一半的 CPU 都没有它。 :P 许多 x86 软件可能没有利用,因为它还不是基线。
    • 如果您打算一次循环一位,您希望通过转移到进位并使用 ADC 来利用进位标志。 (您已经发布了一些 MIPS 答案,所以也许您已经习惯了没有标志的 ISA?)这个函数使用 gcc 和 clang 对于 MSP430 编译成非常糟糕的代码,但我无法让它们使用ADC 采用 x86 的方式。 godbolt.org/z/NQqPGf(使用编译器知道的 tmp = a+a; carry = tmp&lt;a; 成语。)
    【解决方案3】:

    这个逻辑可能比循环略短:

    unsigned char popcnt(unsigned char a)
    {
        a = a - ((a >> 1) & 0x55);            // 2 bit fields 0 -> 2
        a = (a & 0x33) + ((a >> 2) & 0x33);   // 4 bit fields 0 -> 4
        a = (a & 0x0f) +  (a >> 4);           // a = bit count
        return a;
    }
    

    【讨论】:

    • MSP430 的移位有限,但事实证明它可以在立即数为 1..4 (win.tue.nl/~johanl/educ/RTcourse/MSP430%20-%20general.pdf) 的情况下右移,而每条指令只有一位以及许多其他移位。不幸的是,编译器很笨(MSP430 的 gcc 和 clang)并且做得不好,例如重复使用算术右移。 godbolt.org/z/NQqPGf。对于紧凑的代码,您显然希望转换为进位,然后将 ADC 转换为累加器,但我也无法让 gcc 这样做。
    • @PeterCordes - 我的回答描述了逻辑。我的假设是这将是用汇编编写的,但我不知道 MSP430 指令集,并且我也明确表示它可以更短,但我再次不知道 MSP430。
    • 我明白这一点。我的观点是这种逻辑可能不是在 MSP430 上进行 popcount 的有效方法,即使您手动优化了 asm。根据我找到的 PDF,即使您使用移位 4 指令,超过 1 的移位计数显然每次计数需要 1 个周期。对于 8 位,我认为当a 变为零时停止循环可能是一个不错的选择。我的回答表明您可以在循环中将其降低到 3 个 insns。 (顺便说一句,这不是我的反对意见。)
    • @PeterCordes - 这只是一个建议。在超过 15,000 人时,我不再关心否决票了。
    猜你喜欢
    • 1970-01-01
    • 2015-07-09
    • 2017-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-18
    相关资源
    最近更新 更多