有些算法对 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。它有JL 和JGE 在(N XOR V) 上的分支(即有符号比较),但N 将等于MSB,所以N ^ V 只是C,在我们的左移V 设置V = N ^ C 之后.我想你必须从标志寄存器中取出标志字并移位/屏蔽它!或者测试那个标志位和 JNZ。