返回64位整数中所有设置位的最快方法是什么?

时间:2013-12-20 22:35:54

标签: c++ c performance optimization bit-manipulation

我需要一种快速的方法来获取64位整数中所有位的位置。例如,给定x = 123703,我想填充数组idx[] = {0, 1, 2, 4, 5, 8, 9, 13, 14, 15, 16}。我们可以假设我们先验地知道比特数。这将被称为10 ^ 12 - 10 ^ 15次,因此速度至关重要。到目前为止,我提出的最快答案是以下怪物,它使用64位整数的每个字节作为表的索引,这些表给出了在该字节中设置的位数和位置:

int64_t x;            // this is the input
unsigned char idx[K]; // this is the array of K bits that are set
unsigned char *dst=idx, *src;
unsigned char zero, one, two, three, four, five;  // these hold the 0th-5th bytes
zero  =  x & 0x0000000000FFUL;
one   = (x & 0x00000000FF00UL) >> 8;
two   = (x & 0x000000FF0000UL) >> 16;
three = (x & 0x0000FF000000UL) >> 24;
four  = (x & 0x00FF00000000UL) >> 32;
five  = (x & 0xFF0000000000UL) >> 40;
src=tab0+tabofs[zero ]; COPY(dst, src, n[zero ]);
src=tab1+tabofs[one  ]; COPY(dst, src, n[one  ]);
src=tab2+tabofs[two  ]; COPY(dst, src, n[two  ]);
src=tab3+tabofs[three]; COPY(dst, src, n[three]);
src=tab4+tabofs[four ]; COPY(dst, src, n[four ]);
src=tab5+tabofs[five ]; COPY(dst, src, n[five ]);

其中COPY是要复制最多8个字节的switch语句,n是字节中设置的位数的数组,tabofs将偏移量赋予tabX },它保存第X个字节中的设置位的位置。 这比在我的Xeon E5-2609上使用__builtin_ctz()的展开的基于循环的方法快3倍。(见下文。)我目前正在按字典顺序迭代x给定的位数。

有更好的方法吗?

编辑:添加了一个示例(我后来修复了)。完整代码可在此处获取:http://pastebin.com/79X8XL2P。注意:带有-O2的GCC似乎可以优化它,但英特尔的编译器(我曾经编写它)不会...

另外,让我提供一些额外的背景来解决下面的一些评论。目标是对N个可能的解释变量范围内的每个可能的K变量子集进行统计检验;现在的具体目标是N = 41,但我可以看到一些项目需要N到45-50。该测试基本上涉及分解相应的数据子矩阵。在伪代码中,类似这样:

double doTest(double *data, int64_t model) {
  int nidx, idx[];
  double submatrix[][];
  nidx = getIndices(model, idx);  // get the locations of ones in model
  // copy data into submatrix
  for(int i=0; i<nidx; i++) {
    for(int j=0; j<nidx; j++) {
      submatrix[i][j] = data[idx[i]][idx[j]];
    }
  }
  factorize(submatrix, nidx);
  return the_answer;
}

我为英特尔Phi板编写了一个这样的版本,它应该在大约15天内完成N = 41的案例,其中约5-10%的时间花在一个天真的getIndices()上。蝙蝠更快的版本可以节省一天或更多。我正在为NVidia Kepler开发一个实现,但遗憾的是我遇到的问题(很少的小矩阵运算)并不适合硬件(非常大的矩阵运算)。也就是说,this paper提出了一个解决方案,通过积极展开循环并在寄存器中执行整个分解,似乎在我的大小矩阵上实现了数百GFLOPS / s,并注意矩阵的维度在编译时定义-时间。 (这个循环展开应该有助于减少开销并改善Phi版本中的矢量化,所以getIndices()将变得更加重要!)所以现在我认为我的内核看起来应该更像:

double *data;  // move data to GPU/Phi once into shared memory
template<unsigned int K> double doTestUnrolled(int *idx) {
  double submatrix[K][K];
  // copy data into submatrix
  #pragma unroll
  for(int i=0; i<K; i++) {
    #pragma unroll
    for(int j=0; j<K; j++) {
      submatrix[i][j] = data[idx[i]][idx[j]];
    }
  }
  factorizeUnrolled<K>(submatrix);
  return the_answer;
}

Phi版本在`cilk_for'循环中从model = 0到2 ^ N(或者更确切地说,是用于测试的子集)解决每个模型,但现在为了批处理GPU工作并分摊内核启动开销我必须按字典顺序迭代每个K = 1到41位的模型编号(如doynax所述)。

编辑2:现在休假结束了,我的Xeon E5-2602使用icc版本15获得了一些结果。我用来进行基准测试的代码在这里:http://pastebin.com/XvrGQUat。我对具有完全K位设置的整数执行位提取,因此在下表的“基本”列中测量的词典迭代有一些开销。这些进行2 ^ 30次,N = 48(必要时重复)。

“CTZ”是一个循环,它使用gcc内在__builtin_ctzll来获得最低位集:

for(int i=0; i<K; i++) {
    idx[i] = __builtin_ctzll(tmp);
    lb = tmp & -tmp;    // get lowest bit
    tmp ^= lb;      // remove lowest bit from tmp
} 

Mark是Mark的无分支循环:

for(int i=0; i<K; i++) {
    *dst = i;
    dst += x & 1;
    x >>= 1;
} 

Tab1是我原始的基于表格的代码,带有以下副本宏:

#define COPY(d, s, n) \
switch(n) { \
case 8: *(d++) = *(s++); \
case 7: *(d++) = *(s++); \
case 6: *(d++) = *(s++); \
case 5: *(d++) = *(s++); \
case 4: *(d++) = *(s++); \
case 3: *(d++) = *(s++); \
case 2: *(d++) = *(s++); \
case 1: *(d++) = *(s++); \
case 0: break;        \
}

Tab2与Tab1的代码相同,但是复制宏只移动8个字节作为单个副本(从doynax和LưuVĩnhPhúc中获取想法......但请注意,确保对齐) :

#define COPY2(d, s, n) { *((uint64_t *)d) = *((uint64_t *)s); d+=n; }

以下是结果。我想我最初声称Tab1比CTZ快3倍只适用于大K(我在测试的地方)。 Mark的循环比我的原始代码更快,但是摆脱COPY2宏中的分支需要花费K&gt; 8。

 K    Base    CTZ   Mark   Tab1   Tab2
001  4.97s  6.42s  6.66s 18.23s 12.77s
002  4.95s  8.49s  7.28s 19.50s 12.33s
004  4.95s  9.83s  8.68s 19.74s 11.92s
006  4.95s 16.86s  9.53s 20.48s 11.66s
008  4.95s 19.21s 13.87s 20.77s 11.92s
010  4.95s 21.53s 13.09s 21.02s 11.28s
015  4.95s 32.64s 17.75s 23.30s 10.98s
020  4.99s 42.00s 21.75s 27.15s 10.96s
030  5.00s 100.64s 35.48s 35.84s 11.07s
040  5.01s 131.96s 44.55s 44.51s 11.58s

11 个答案:

答案 0 :(得分:7)

我认为这里表现的关键是关注更大的问题,而不是微观优化从随机整数中提取位位置。

根据您的示例代码和之前的SO问题判断,您将枚举所有按顺序设置K位的字,并从中提取位索引。这大大简化了事情。

如果是这样,那么不是每次迭代重建位位置而是直接递增位数组中的位置。一半时间这将涉及单循环迭代和增量。

这些方面的东西:

// Walk through all len-bit words with num-bits set in order
void enumerate(size_t num, size_t len) {
    size_t i;
    unsigned int bitpos[64 + 1];

    // Seed with the lowest word plus a sentinel
    for(i = 0; i < num; ++i)
        bitpos[i] = i;
    bitpos[i] = 0;

    // Here goes the main loop
    do {
        // Do something with the resulting data
        process(bitpos, num);

        // Increment the least-significant series of consecutive bits
        for(i = 0; bitpos[i + 1] == bitpos[i] + 1; ++i)
            bitpos[i] = i;
    // Stop on reaching the top
    } while(++bitpos[i] != len);
}

// Test function
void process(const unsigned int *bits, size_t num) {
    do
        printf("%d ", bits[--num]);
    while(num);
    putchar('\n');
}

没有特别优化,但你得到了一般的想法。

答案 1 :(得分:6)

这里有一些非常简单的东西可能更快 - 没有测试就无法知道。很大程度上取决于设置的位数与未设置的数量。你可以展开这个以完全删除分支,但是对于今天的处理器,我不知道它是否会加速。

unsigned char idx[K+1]; // need one extra for overwrite protection
unsigned char *dst=idx;
for (unsigned char i = 0; i < 50; i++)
{
    *dst = i;
    dst += x & 1;
    x >>= 1;
}

P.S。问题中的示例输出是错误的,请参阅http://ideone.com/2o032E

答案 2 :(得分:3)

作为最小修改:

int64_t x;            
char idx[K+1];
char *dst=idx;
const int BITS = 8;
for (int i = 0 ; i < 64+BITS; i += BITS) {
  int y = (x & ((1<<BITS)-1));
  char* end = strcat(dst, tab[y]); // tab[y] is a _string_
  for (; dst != end; ++dst)
  {
    *dst += (i - 1); // tab[] is null-terminated so bit positions are 1 to BITS.
  }
  x >>= BITS;
}

BITS的选择决定了表格的大小。 8,13和16是逻辑选择。每个条目都是一个字符串,以零结尾并包含1个偏移的位位置。即tab [5]是"\x03\x01"。内部循环修复了这个偏移。

效率稍高:用

替换strcat和内循环
char const* ptr = tab[y];
while (*ptr)
{
   *dst++ = *ptr++ + (i-1);
}

如果循环包含分支,循环展开可能会有点痛苦,因为复制这些分支语句对分支预测器没有帮助。我很乐意将这个决定留给编译器。

我正在考虑的一件事是tab[y]是一个指向字符串的指针数组。这些高度相似:"\x1""\x3\x1"的后缀。实际上,不以"\x8"开头的每个字符串都是字符串的后缀。我想知道你需要多少独特的字符串,以及tab[y]实际需要的程度。例如。根据上面的逻辑,tab[128+x] == tab[x]-1

[编辑]

没关系,你肯定需要以"\x8"开头的128个标签条目,因为它们永远不是另一个字符串的后缀。尽管如此,tab[128+x] == tab[x]-1规则意味着您可以节省一半的条目,但需要额外支付两条指令:char const* ptr = tab[x & 0x7F] - ((x>>7) & 1)。 (在<{em> tab[]

之后设置\x8指向

答案 3 :(得分:2)

使用char不会帮助您提高速度,但实际上在计算时通常需要更多的ANDing和符号/零扩展。只有在适合缓存的非常大的数组的情况下,才应使用较小的int类型

您可以改进的另一件事是COPY宏。如果可能,请复制整个单词,而不是逐字节复制

inline COPY(unsigned char *dst, unsigned char *src, int n)
{
switch(n) { // remember to align dst and src when declaring
case 8:
    *((int64_t*)dst) = *((int64_t*)src);
    break;
case 7:
    *((int32_t*)dst) = *((int32_t*)src);
    *((int16_t*)(dst + 4)) = *((int32_t*)(src + 4));
    dst[6] = src[6];
    break;
case 6:
    *((int32_t*)dst) = *((int32_t*)src);
    *((int16_t*)(dst + 4)) = *((int32_t*)(src + 4));
    break;
case 5:
    *((int32_t*)dst) = *((int32_t*)src);
    dst[4] = src[4];
    break;
case 4:
    *((int32_t*)dst) = *((int32_t*)src);
    break;
case 3:
    *((int16_t*)dst) = *((int16_t*)src);
    dst[2] = src[2];
    break;
case 2:
    *((int16_t*)dst) = *((int16_t*)src);
    break;
case 1:
    dst[0] = src[0];
    break;
case 0:
    break;
}

此外,由于tabofs [x]和n [x]通常彼此接近,请尝试将其放在内存中以确保它们始终在缓存中

typedef struct TAB_N
{
    int16_t n, tabofs;
} tab_n[256];

src=tab0+tab_n[b0].tabofs; COPY(dst, src, tab_n[b0].n);
src=tab0+tab_n[b1].tabofs; COPY(dst, src, tab_n[b1].n);
src=tab0+tab_n[b2].tabofs; COPY(dst, src, tab_n[b2].n);
src=tab0+tab_n[b3].tabofs; COPY(dst, src, tab_n[b3].n);
src=tab0+tab_n[b4].tabofs; COPY(dst, src, tab_n[b4].n);
src=tab0+tab_n[b5].tabofs; COPY(dst, src, tab_n[b5].n);

最后但并非最不重要的是,gettimeofday不适用于性能计算。请改用QueryPerformanceCounter,它更准确

答案 4 :(得分:1)

您的代码使用的是1字节(256个条目)的索引表。如果使用2字节(65536个条目)索引表,则可以将其加速2倍。

不幸的是,你可能无法进一步扩展 - 因为3字节的表大小将是16MB,不太可能适合CPU本地缓存,而且只会使速度变慢。

答案 5 :(得分:0)

问题是你要对收集职位做些什么? 如果你必须多次迭代它,那么是的,现在你可以收集它们一次,并且迭代很多。 但是如果它只是迭代一次或几次,那么你可能会想到不创建一个中间位置数组,只需在每次遇到1时调用一个处理块闭包/函数,同时迭代位。

这是我在Smalltalk中编写的位迭代器的一个简单例子:

LargePositiveInteger>>bitsDo: aBlock
| mask offset |
1 to: self digitLength do: [:iByte |
    offset := (iByte - 1) << 3.
    mask := (self digitAt: iByte).
    [mask = 0]
        whileFalse:
            [aBlock value: mask lowBit + offset.
            mask := mask bitAnd: mask - 1]]

LargePositiveInteger是由字节数组成的任意长度的整数。 lowBit回答最低位的等级,并实现为具有256个条目的查找表。

在C ++ 2011中,您可以轻松传递一个闭包,因此它应该很容易翻译。

uint64_t x;
unsigned int mask;
void (*process_bit_position)(unsigned int);
unsigned char offset = 0;
unsigned char lowBitTable[16] = {0,0,1,0,2,0,1,0,3,0,1,0,2,0,1,0}; // 0-based, first entry is unused
while( x )
{
    mask = x & 0xFUL;
    while (mask)
    {
        process_bit_position( lowBitTable[mask]+offset );
        mask &= mask - 1;
    }
    offset += 4;
    x >>= 4;
}

使用4位表演示了该示例,但如果它适合缓存,则可以轻松地将其扩展到13位或更多。

对于分支预测,内部循环可以重写为for(i=0;i<nbit;i++),附加表nbit=numBitTable[mask]然后用开关展开(编译器可以做到吗?),但是我让你测量一下首先执行......

答案 6 :(得分:0)

发现这个太慢了吗?
小而粗,但它都在缓存和CPU寄存器中;

void mybits(uint64_t x, unsigned char *idx)
{
  unsigned char n = 0;
  do {
    if (x & 1) *(idx++) = n;
    n++;
  } while (x >>= 1);          // If x is signed this will never end
  *idx = (unsigned char) 255; // List Terminator
}

展开循环并产生64个真/假值的数组(这不是你想要的)的速度仍然快3倍

void mybits_3_2(uint64_t x, idx_type idx[])
{
#define SET(i) (idx[i] = (x & (1UL<<i)))
  SET( 0);
  SET( 1);
  SET( 2);
  SET( 3);
  ...
  SET(63);
}

答案 7 :(得分:0)

这是一些严格的代码,为1字节(8位)编写,但它应该很容易,显然扩展到64位。

int main(void)
{
    int x = 187;

    int ans[8] = {-1,-1,-1,-1,-1,-1,-1,-1};
    int idx = 0;

    while (x)
    {
        switch (x & ~(x-1))
        {
        case 0x01: ans[idx++] = 0; break;
        case 0x02: ans[idx++] = 1; break;
        case 0x04: ans[idx++] = 2; break;
        case 0x08: ans[idx++] = 3; break;
        case 0x10: ans[idx++] = 4; break;
        case 0x20: ans[idx++] = 5; break;
        case 0x40: ans[idx++] = 6; break;
        case 0x80: ans[idx++] = 7; break;
        }

        x &= x-1;
    }

   getchar();
   return 0;
}

输出数组应为:

ans = {0,1,3,4,5,7,-1,-1};

答案 8 :(得分:0)

如果我采取“我需要一种快速的方法来获取64位整数中所有位的位置”字面意思......

我意识到这已经有几个星期了,然而出于好奇,我记得回到我的装配日,CBM64和Amiga使用算术移位然后检查进位标志 - 如果已设置则移位位是1,如果清除则为零

e.g。对于算术移位(从第64位到第0位检查)....

pseudo code (ignore instruction mix etc errors and oversimplification...been a while):

    move #64+1, counter
    loop. ASL 64bitinteger       
    BCS carryset
    decctr. dec counter
    bne loop
    exit

    carryset. 
    //store #counter-1 (i.e. bit position) in datastruct indexed by counter
    jmp decctr

......我希望你明白这一点。

从那时起我就没有使用过汇编但是我想知道我们是否可以使用类似于上面的一些C ++内联汇编来做类似的事情。我们可以在汇编(非常少的代码行)中进行整个转换,构建适当的数据结构。 C ++可以简单地检查答案。

如果可以,那么我想它会很快。

答案 9 :(得分:0)

假设设置位数的稀疏性,

SCOPE_IDENTITY()

答案 10 :(得分:0)

一个简单的解决方案,但可能不是最快的,这取决于 log 和 pow 函数的时间:

#include<math.h>

void getSetBits(unsigned long num){

    int bit;
    while(num){
        bit = log2(num);
        num -= pow(2, bit);

        printf("%i\n", bit); // use bit number
    }
}

复杂度 O(D) | D 是设置位数。