如何在Sandy Bridge上的一系列int中快速计数位到单独的箱子中
How to quickly count bits into separate bins in a series of ints on Sandy Bridge?
更新:请阅读代码,它不是关于计数位在一个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和一半的1)
您可以尝试使用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个。对于每次迭代,我们从bits
加载4位,然后将它们用作LUT的索引,LUT包含当前4位的4个增量的所有可能组合。然后将这4个增量加到bit_counter当前的4个元素上。
加载、存储和添加的数量减少了1/4,但这将在一定程度上被LUT加载和其他内务管理所抵消。你可能仍然会看到2倍的速度提升。如果你决定尝试一下,我很想知道结果。
也许你可以一次做8个,以8为间隔取8位,并保留8个uint64。每个计数器只有1个字节,所以你只能累积255个count
调用,然后才能解包那些uint64。
看Bit Twiddling Hacks
- 计数位设置
- 计数位设置,朴素方式
- 查找表设置的计数位
- 计数位设置,Brian Kernighan的方式
- 使用64位指令计数12位、24位或32位的字节
- 计数位设置,并行
- 从最高有效位到给定位置的位数设置(秩)
- 选择给定计数(秩)的位位置(从最高有效位开始)
Edit至于"位位置桶积累"(bit_counter[]
),我有一种感觉,这可能是一个很好的valarrays + masking案例。这将是相当多的编码+测试+分析。如果你真的感兴趣,请告诉我。
显然这可以用"垂直计数器"快速完成。来自@steike:
关于比特技巧(存档)的现已关闭的页面考虑一个普通的整数数组,我们读取其中的位水平:
msb<-->lsb x[0] 00000010 = 2 x[1] 00000001 = 1 x[2] 00000101 = 5
一个垂直计数器存储数字,顾名思义,垂直;也就是说,一个k位计数器存储在k个单词中每个字一个位
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次——迭代次数由最长的进位链决定。
您可以像这样展开函数。它可能比编译器所能做的要快!
// 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
//...
您可以使用一组计数器,每个计数器的大小不同。首先在2位计数器中累积3个值,然后解包它们并更新4位计数器。当15个值准备好时,解包为字节大小的计数器,在255个值之后更新bit_counter[]。
所有这些工作都可以在128位SSE寄存器中并行完成。在现代处理器上,只需要一条指令就可以将1位解包为2位。只需将源四字与PCLMULQDQ指令相乘即可。这将使源位与零交错。同样的技巧可以帮助将2位解包为4位。4位和8位的解包可以通过洗牌、解包和简单的逻辑操作来完成。
平均性能似乎很好,但代价是额外的计数器120字节和相当多的汇编代码。
一般来说没有办法回答这个问题;这完全取决于编译器以及底层架构。唯一真正知道的方法就是去尝试不同的解决方案和测量。(例如,在某些机器上,轮班可能非常昂贵。在其他方面,没有。)首先,我会用例如:
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);
可能更好。或者更糟……提前知道是不可能的。这是也有可能在某些机器上,系统地切换到低阶位和掩码,如你所做的,是最好的。
一些优化还将取决于典型数据的样子。如果大多数单词只有一个或两个位,你可能会受益每次测试一个字节,或者一次测试四个比特,然后跳过这些都是0。
如果计算每次偏移(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];
}
}
}
}
}
这是相当快的:
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位的ff。对于大多数编译器和CPU来说,这是一条指令。对于字中的每个位执行一次循环,因此bits=0
将非常快,而1
的64位将会较慢。
我在64位Ubuntu和GCC下测试了这个,它产生的数据输出与你的:
void Count(uint64 bits) {
bit_counter[0] += (bits >> 0) & 1;
bit_counter[1] += (bits >> 1) & 1;
// ..
bit_counter[63] += (bits >> 63) & 1;
}
速度根据64位字中1
位的个数而变化。
- 如何在C++中从两个单独的for循环中添加两个数组
- 用C++在单独的头文件中完成函数体
- 类模板的成员功能的定义在单独的TU中完全专业化
- 如何使用单独文件中的派生类访问友元函数对象
- CoInitialize()在单独的线程上崩溃而不返回
- avrogencpp能为模式中的每种类型生成单独的头文件吗
- 转换函数,将 std::数组的双精度作为参数或双精度作为参数单独转换
- 单独定义模板化嵌套类方法的正确语法
- 如何将 2 个类分成单独的 .h 文件并正确设置它们
- C++ 链接到单独的.cpp文件说"multiple definitions"
- 从单独的文件中读取树
- 将字符串拆分为标记,并将标记拆分为两个单独的数组
- C++计算器,不能单独除以 0
- 自 Windows 10 20H1 以来,具有单独线程的多个窗口停止工作
- 当结构位于单独的头文件中时'unregistered datatype'在 QML 中出现Q_GADGET错误
- 通过单独的 tcp 流建立 http 连接
- Cmake:为 C 设置警告级别,并为 MSVC 单独设置C++代码(Visual C++)
- 如何将字符串和整数读取到两个单独的动态数组中的程序编写?
- 如何从单独的线程控制 SFML 窗口?
- 如何在Sandy Bridge上的一系列int中快速计数位到单独的箱子中