您可以做的最好的想法是使用Paul R 建议的内置popcount
函数,但是由于您需要在汇编中编写它,这对我有用:
asm (
"start: \n"
"and %0, %1 \n"
"jz end \n"
"shr $0, %1 \n"
"jnc start \n"
"inc %1 \n"
"jmp start \n"
"end: \n"
: "+g" (result),
"+r" (x)
:
: "cc"
);
在前两行中,您只需检查x
(如果为零,则转到 end Jump Zero
)的内容。比你x
向右移动一位并且:
在移位操作结束时,CF
标志包含从目标操作数移出的最后一位。*
如果没有CF
设置,只需转到开始(Jump Not Carry
),否则增加结果然后开始。
组装的美妙之处在于您可以通过多种方式做事……
asm (
"start: \n"
"shr $1, %1 \n"
"jnc loop_cond \n"
"inc %0 \n"
"and %1, %1 \n"
"loop_cond: \n"
"jnz start \n"
: "+g" (result),
"+r" (x)
:
: "cc"
);
在这里您再次使用SHift Right
指令,如果不CF
存在,则进入循环条件。
否则再次增加结果并调用二进制AND
(INC
确实修改ZF
)。
使用LOOP
和ECX
我很好奇如何在 3 条指令中做到这一点(如果不可能的话,我认为你的老师不会给你 3 的下限)并且我意识到 x86 也有LOOP
指令:
每次执行 LOOP 指令时,计数寄存器递减,然后检查是否为 0。如果计数为 0,则终止循环并继续执行 LOOP 指令之后的指令。如果计数不为零,则执行近跳转到目标(目标)操作数,这可能是循环开始处的指令。*
您可以使用GCC 输入约束添加输入参数:
c
-c
登记册。
asm (
"start: \n"
"shr $1, %1 \n"
"adc $0, %0 \n"
"loop start \n"
: "+g" (result)
: "r" (x),
"c" (8) // Assuming 8b type (char)
);
只是为了确保它编译为正确的程序集:
0x000000000040051f <+25>: mov $0x8,%ecx
0x0000000000400524 <+30>: mov -0x8(%rbp),%eax
0x0000000000400527 <+33>: shr %edx
0x0000000000400529 <+35>: adc $0x0,%eax
0x000000000040052c <+38>: loop 0x400527 <main+33>
我认为第一个应该有更好的性能,特别是如果只有 1 位设置,这种方法总是k*8
迭代。
SSE4 和单指令
我知道你必须使用循环,但只是为了好玩......使用SSE4 扩展,你可以通过一条指令来做到这一点POPCNT
:
该指令计算第二个操作数(源)中设置为 1 的位数,并返回第一个操作数(目标寄存器)中的计数。*
我认为(我的笔记本电脑上有一个相当旧的 CPU,所以我无法为你测试这个)你应该能够只用一个简单的指令来做到这一点:
asm (
"POPCNT %1, %0 \n"
: "=r" (result)
: "mr" (x)
: "cc"
);
(如果你尝试这个并且你确实有 SSE4 扩展,请让我知道它是否有效)
表现
我测量了进行 100,000,000 次 popcounts 所需的时间,并将我的第一种和第二种方法与David Wohlferd 的. [原始数据]
+--------------+------------+------------+------------+
| | 0x00000000 | 0x80000001 | 0xffffffff |
+--------------+------------+------------+------------+
| 1st solution | 0.543 | 5.040 | 3.833 |
| LOOP | 11.530 | 11.523 | 11.523 |
| Davids | 0.750 | 4.893 | 4.890 |
+--------------+------------+------------+------------+
如果有人可以将这 3 个与 SSE4 的POPCNT
指令进行比较,我会很高兴。