更新:请阅读代码,它不是关于计算一个int中的位
是否可以使用一些聪明的汇编程序来提高以下代码的性能?
uint bit_counter[64];
void Count(uint64 bits) {
bit_counter[0] += (bits >> 0) & 1;
bit_counter[1] += (bits >> 1) & 1;
// ..
bit_counter[63] += (bits >> 63) & 1;
}
Count
位于我算法的最内层循环中。
更新 架构:x86-64,Sandy Bridge,所以可以使用SSE4.2,AVX1和旧技术,但不能使用AVX2或BMI1 / 2。
bits
变量几乎是随机位(接近半零和一半)
答案 0 :(得分:8)
您可以尝试使用SSE,每次迭代增加4个元素。
警告:未经测试的代码如下......
#include <stdint.h>
#include <emmintrin.h>
uint32_t bit_counter[64] __attribute__ ((aligned(16)));
// make sure bit_counter array is 16 byte aligned for SSE
void Count_SSE(uint64 bits)
{
const __m128i inc_table[16] = {
_mm_set_epi32(0, 0, 0, 0),
_mm_set_epi32(0, 0, 0, 1),
_mm_set_epi32(0, 0, 1, 0),
_mm_set_epi32(0, 0, 1, 1),
_mm_set_epi32(0, 1, 0, 0),
_mm_set_epi32(0, 1, 0, 1),
_mm_set_epi32(0, 1, 1, 0),
_mm_set_epi32(0, 1, 1, 1),
_mm_set_epi32(1, 0, 0, 0),
_mm_set_epi32(1, 0, 0, 1),
_mm_set_epi32(1, 0, 1, 0),
_mm_set_epi32(1, 0, 1, 1),
_mm_set_epi32(1, 1, 0, 0),
_mm_set_epi32(1, 1, 0, 1),
_mm_set_epi32(1, 1, 1, 0),
_mm_set_epi32(1, 1, 1, 1)
};
for (int i = 0; i < 64; i += 4)
{
__m128i vbit_counter = _mm_load_si128(&bit_counter[i]);
// load 4 ints from bit_counter
int index = (bits >> i) & 15; // get next 4 bits
__m128i vinc = inc_table[index]; // look up 4 increments from LUT
vbit_counter = _mm_add_epi32(vbit_counter, vinc);
// increment 4 elements of bit_counter
_mm_store_si128(&bit_counter[i], vbit_counter);
} // store 4 updated ints
}
它是如何工作的:基本上我们在这里所做的就是对原始循环进行矢量化,这样我们每循环迭代处理4位而不是1.所以我们现在有16次循环迭代而不是64次。对于每次迭代,我们加载4位来自bits
,然后将它们用作LUT的索引,其中包含当前4位的4个增量的所有可能组合。然后,我们将这4个增量添加到bit_counter的当前4个元素中。
加载和存储及添加的数量减少了4倍,但这将由LUT负载和其他内务处理稍微抵消。你可能仍然看到加速2倍。如果您决定尝试,我会对结果感兴趣。
答案 1 :(得分:7)
也许你可以一次做8个,将8位间隔8个并保持8个uint64的计数。这只是每个单个计数器只有1个字节,因此在你必须解压缩那些uint64之前,你只能累积255个count
的调用。
答案 2 :(得分:4)
编辑至于“比特位置桶积累”(bit_counter[]
)我觉得这可能是valarrays + masking的一个好例子。这虽然是一些编码+测试+分析。如果你真的感兴趣,请告诉我。
现在,您可以使用绑定元组(TR1,boost或C ++ 11)非常接近valarray行为;我觉得它会更容易阅读并且编译速度更慢。
答案 3 :(得分:4)
显然,这可以通过“垂直计数器”快速完成。从now-defunct page on Bit tricks(archive)到@steike:
考虑一个正常的整数数组,我们在那里读取这些位 水平:
msb<-->lsb x[0] 00000010 = 2 x[1] 00000001 = 1 x[2] 00000101 = 5
垂直计数器存储数字,顾名思义, 垂直;也就是说,k位计数器存储在k个字之间,带有a 每个单词中的单个位。
x[0] 00000110 lsb ↑ x[1] 00000001 | x[2] 00000100 | x[3] 00000000 | x[4] 00000000 msb ↓ 512
使用这样存储的数字,我们可以使用按位运算 一次性增加它们的任何子集。
我们在对应的位置创建一个位为1位的位图 我们想要递增的计数器,并从LSB向上遍历数组, 我们去的时候更新这些位。从一个添加的“携带”变为 数组的下一个元素的输入。
input sum -------------------------------------------------------------------------------- A B C S 0 0 0 0 0 1 0 1 sum = a ^ b 1 0 0 1 carry = a & b 1 1 1 1 carry = input; long *p = buffer; while (carry) { a = *p; b = carry; *p++ = a ^ b; carry = a & b; }
对于64位字,循环平均运行6-7次 - 迭代次数由最长链的确定。
答案 4 :(得分:3)
您可以像这样展开您的功能。它可能比编译器的速度快!
// rax as 64 bit input
xor rcx, rcx //clear addent
add rax, rax //Copy 63th bit to carry flag
adc dword ptr [@bit_counter + 63 * 4], ecx //Add carry bit to counter[64]
add rax, rax //Copy 62th bit to carry flag
adc dword ptr [@bit_counter + 62 * 4], ecx //Add carry bit to counter[63]
add rax, rax //Copy 62th bit to carry flag
adc dword ptr [@bit_counter + 61 * 4], ecx //Add carry bit to counter[62]
// ...
add rax, rax //Copy 1th bit to carry flag
adc dword ptr [@bit_counter + 1 * 4], ecx //Add carry bit to counter[1]
add rax, rax //Copy 0th bit to carry flag
adc dword ptr [@bit_counter], ecx //Add carry bit to counter[0]
修改强>
您也可以尝试使用双增量:
// rax as 64 bit input
xor rcx, rcx //clear addent
//
add rax, rax //Copy 63th bit to carry flag
rcl rcx, 33 //Mov carry to 32th bit as 0bit of second uint
add rax, rax //Copy 62th bit to carry flag
adc qword ptr [@bit_counter + 62 * 8], rcx //Add rcx to 63th and 62th counters
add rax, rax //Copy 61th bit to carry flag
rcl rcx, 33 //Mov carry to 32th bit as 0bit of second uint
add rax, rax //Copy 60th bit to carry flag
adc qword ptr [@bit_counter + 60 * 8], rcx //Add rcx to 61th and 60th counters
//...
答案 5 :(得分:2)
您可以使用每组不同大小的计数器。首先在2位计数器中累加3个值,然后解压缩它们并更新4位计数器。当15个值准备就绪时,解压缩到字节大小的计数器,并在255个值之后更新bit_counter []。
所有这些工作可以在128位SSE寄存器中并行完成。在现代处理器上,只需要一条指令将1位解包为2.只需将源四字与PCLMULQDQ指令相乘。这将使源位与零交错。相同的技巧可能有助于将2位解包为4.并且可以使用shuffle,unpacks和简单的逻辑运算来解压缩4位和8位。
平均性能似乎不错,但是额外的计数器和相当多的汇编代码的价格是120字节。
答案 6 :(得分:1)
总的来说,没有办法回答这个问题;这一切都取决于编译器 和底层架构。唯一真正知道的方法就是尝试 不同的解决方案和措施。 (例如,在某些机器上, 轮班可能非常昂贵。在其他人,没有。)对于初学者,我会使用 类似的东西:
uint64_t mask = 1;
int index = 0;
while ( mask != 0 ) {
if ( (bits & mask) != 0 ) {
++ bit_counter[index];
}
++ index;
mask <<= 1;
}
完全展开循环可能会提高性能。
根据体系结构,将if
替换为:
bit_counter[index] += ((bits & mask) != 0);
可能会更好。或者更糟糕......提前知道是不可能的。它的 也有可能在某些机器上,系统地转移到 正如你所做的那样,低位和掩蔽是最好的。
某些优化还取决于典型数据的外观。如果 大多数单词只设置了一个或两个位,你可能会获得 一次测试一个字节,或一次测试四个字节,然后跳过这些字节 完全是零。
答案 7 :(得分:1)
如果计算每个偏移(16种可能性)的每个半字节(16种可能性)发生的频率,您可以轻松地对结果求和。这256个总和很容易保留:
unsigned long nibble_count[16][16]; // E.g. 0x000700B0 corresponds to [4][7] and [2][B]
unsigned long bitcount[64];
void CountNibbles(uint64 bits) {
// Count nibbles
for (int i = 0; i != 16; ++i) {
nibble_count[i][bits&0xf]++;
bits >>= 4;
}
}
void SumNibbles() {
for (int i = 0; i != 16; ++i) {
for (int nibble = 0; nibble != 16; ++nibble) {
for(int bitpos = 0; bitpos != 3; ++bitpos) {
if (nibble & (1<<bitpos)) {
bitcount[i*4 + bitpos] += nibble_count[i][nibble];
}
}
}
}
}
答案 8 :(得分:0)
这很快:
void count(uint_fast64_t bits){
uint_fast64_t i64=ffs64(bits);
while(i64){
bit_counter[i64-1]++;
bits=bits & 0xFFFFFFFFFFFFFFFF << i64;
i64=ffs64(bits);
}
}
对于64位,您需要快速实现ffs。对于大多数编译器和CPU来说,这是一条指令。对于字中的每个位,循环执行一次,因此bits=0
将非常快,而1
的64位将更慢。
我使用GCC在64位Ubuntu下进行了测试,它产生与您的相同的数据输出:
void Count(uint64 bits) {
bit_counter[0] += (bits >> 0) & 1;
bit_counter[1] += (bits >> 1) & 1;
// ..
bit_counter[63] += (bits >> 63) & 1;
}
速度可根据64位字中1
位的数量而变化。