获取一个64位整数中每个位的位置的数组

14

好的,这可能听起来有点复杂,但这就是我想做的事情:

  • 拿一个例如10101010101的数字
  • 返回{ 0, 2, 4, 6, 8, 10 } - 一个包含已设置位的所有位置的数组

这是我的代码:

UINT DQBitboard::firstBit(U64 bitboard)
{
    static const int index64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5  };

    static const U64 debruijn64 = 0x07EDD5E59A4E28C2ULL;

    #pragma warning (disable: 4146)
    return index64[((bitboard & -bitboard) * debruijn64) >> 58];  
}

vector<UINT> DQBitboard::bits(U64 bitboard)
{
    vector<UINT> res;

    while (bitboard)
    {
        UINT first = DQBitboard::firstBit(bitboard);
        res.push_back(first);

        bitboard &= ~(1ULL<<first);
    }

    return res;
}

而且这段代码确实有效

我的观点是:

  • 您是否有任何更快的实现方法?
  • 您是否注意到可以优化的内容?如果有,是什么?

提示:

  • UINTunsigned int 的 typedef
  • U64unsigned long long 的 typedef
  • 两种方法都是 static inline

2
@K-ballo 国际象棋引擎编程以及使用位棋盘(64位无符号整数)进行棋盘表示和走子生成。 :-) - Dr.Kameleon
2
你的例子正确吗 - 你似乎返回了奇数位,其中1位在偶数位0、2、4...上。 - Mats Petersson
你期望设置多少位?可能存在一个阈值,低于该阈值时查找表方法更快,高于该阈值时朴素迭代更快。 - hyde
1
请查看http://en.wikipedia.org/wiki/Find_first_set。 - brian beuning
另请参阅@Andrew在https://dev59.com/zeo6XIcBkEYKwwoYKRHd#20719768上的提问。 - 9mjb
显示剩余3条评论
8个回答

10

这里有另一个建议可以进行概要分析(可与其他建议结合以进一步优化)。请注意,此处的循环复杂度为O(设置位数)

vector<UINT> bits_set (UINT64 data) 
{
    UINT n;
    vector<UINT> res;
    res.reserve(64);
    for (n = 0; data != 0; n++, data &= (data - 1))
    {
        res.push_back(log2(data & ~(data-1)));
    }
    return res;
}

哇!!! 是的,那个解决了问题。顺便说一下,相比于我的版本,提高了约20%的速度...印象深刻。 - Dr.Kameleon
我会选择这个作为答案 - 因为对于我的情况来说,它是迄今为止最快的。然而,我仍然愿意听取新的建议。再次感谢,伙计! :-) - Dr.Kameleon
log2是来自问题的DeBruijn64查找表吗?还是一些更快的实现? - Ben Voigt
@BenVoigt 这是二进制对数函数(以2为底的对数)。 - Dr.Kameleon
1
@Dr.Kameleon:我知道这个行为是什么,但我不知道实现方式是什么(这将决定该方法的性能成败)。 - Ben Voigt
2
@BenVoigt 这是一个好问题。用户可以决定log2的实现方式,如果足够好-算法不限制任何特定的实现方式;这就是为什么我写了“此建议可以进行分析”。还应该注意到,算法的性能直接与输入中设置位的数量相关,对于所有位都设置的情况,它不会比简单的迭代更好。 - SomeWittyUsername

7

位移操作非常廉价。查找表会消耗缓存空间,而且查找表还需要整数乘法。我认为直接暴力计算比聪明的技巧更快。

vector<UINT> DQBitboard::bits(U64 bitboard)
{
    vector<UINT> res;
    res.reserve(64);
    uint_fast8_t pos = 1;

    do {
        if (bitboard & 1) res.push_back(pos);
        ++pos;
    } while (bitboard >>= 1);

    return res;
}

您可以稍微展开循环,这可能会使其更快。

std::vector 是迄今为止最昂贵的部分。考虑直接使用位板(bitboard)。例如:

struct bitboard_end_iterator{};
struct bitboard_iterator
{
    U64 value;
    uint_fast8_t pos;

    bitboard_iterator(U64 bitboard) : value(bitboard), pos(0)
    {
        ++(*this);
    }
    UINT operator*() const { return pos + 1; }
    bool operator==( bitboard_end_iterator ) const { return pos == 64; }
    operator bool() const { return pos < 64; }
    bitboard_iterator& operator++()
    {
        while (U64 prev = value) {
            value >>= 1;
            ++pos;
            if (prev & 1) return *this;
        }
        pos = 64;
        return *this;
    }
};

现在你可以编写

for( bitboard_iterator it(bitboard); it; ++it )
    cout << *it;

我认为你会得到你的比特币列表。
第二版:
class bitboard_fast_iterator
{
    U64 value;
    uint_fast8_t pos;

public:
    bitboard_fast_iterator(U64 bitboard = 0) : value(bitboard), pos(__builtin_ctzll(value)) {}
    UINT operator*() const { return pos + 1; }
    operator bool() const { return value != 0; }
    bitboard_iterator& operator++()
    {
        value &= ~(1ULL << pos);
        pos = __builtin_ctzll(value);
        return *this;
    }
};

@Dr.Kameleon:没错。但是你是在对运行在实际棋盘引擎中的循环进行基准测试,还是仅包含此函数的循环?在微基准测试中,查找表将轻松适合缓存中。在您的实际代码中,查找表可能被驱逐以支持您正在处理的其他数据。 - Ben Voigt
@Dr.Kameleon:好的,这是正确的做法。如果可以的话,我会摆脱std::vector。我认为你可以创建一个bitboard_iterator对象,它可以与所有向量算法一起使用,但速度更快。 - Ben Voigt
非常感谢您的建议。我会尝试一下。也许,与我的版本和其他版本之间的速度差异是@MatsPetersson猜测的原因:相对于0,我的常规比特板中包含非常少的1... 无论如何,再次感谢! :-) - Dr.Kameleon
1
@Dr.Kameleon:看看那个迭代器是否适合你?当然,你也可以使用DeBruijn查找来完成。 - Ben Voigt
+1 我喜欢迭代器的方法,并在自己的棋盘游戏引擎中使用它。为了更好地量化,我将 U64 封装在一个类中,并定义 begin()end() 迭代器。这使我能够编写 std::copy(begin(bitboard), end(bitboard), std::back_insert(res)); 迭代器的 operator++operator* 使用与 g++ Find_firstFind_next 扩展到 std::bitset 相同的技术实现(并使用 __builtin_ctzll 来定位第一个/下一个位)。 - TemplateRex
显示剩余4条评论

6

我一直在想,使用最佳的汇编指令是否能使程序更快。所以我尝试了三种实现方式,并得到了1000万次迭代的以下结果:

您的实现(Dr.Kameleon)1.77秒

log2()实现(icepack)2.17秒

我的汇编实现(我自己)0.16秒

输出:

bits version:
Function started at 0
           ended at 177
              spent 177 (1.770000 seconds)
c version:
Function started at 177
           ended at 394
              spent 217 (2.170000 seconds)
c version:
Function started at 394
           ended at 410
              spent 16 (0.160000 seconds)

关于C/C++,静态编译实际上是将其编译成一系列CPU指令(这并不是我所期望的!)。相反地,可以在一个匿名命名空间中使用函数外的数组来达到预期效果。尽管在汇编中可以使用.long(或其他大小),然后使用%rip从IP引用数据。
注意:一旦编译完成,我在汇编版本中没有看到大小(n)被使用,因此我不太确定返回的数组是否有效。除此之外,代码本身成为了5个汇编指令的循环,因此速度略微提高(约为x10)。 log2()运行缓慢的原因是它将数字转换为xmm寄存器,然后调用另一个函数。然后将xmm寄存器转换回常规寄存器。
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/times.h>
#include <string.h>
#include <math.h>
#include <vector>

using namespace std;

namespace
{
const int index64[64] = {
    63,  0, 58,  1, 59, 47, 53,  2,
    60, 39, 48, 27, 54, 33, 42,  3,
    61, 51, 37, 40, 49, 18, 28, 20,
    55, 30, 34, 11, 43, 14, 22,  4,
    62, 57, 46, 52, 38, 26, 32, 41,
    50, 36, 17, 19, 29, 10, 13, 21,
    56, 45, 25, 31, 35, 16,  9, 12,
    44, 24, 15,  8, 23,  7,  6,  5  };
const uint64_t debruijn64 = 0x07EDD5E59A4E28C2ULL;
}

int firstBit(uint64_t bitboard)
{
    return index64[((bitboard & -bitboard) * debruijn64) >> 58];  
}

vector<int> bits(uint64_t bitboard)
{
    vector<int> res;
    res.reserve(64);

    while(bitboard)
    {
        int first = firstBit(bitboard);
        res.push_back(first);

        bitboard &= ~(1ULL << first);
    }
    return res;
}



vector<int> bits_c(uint64_t bitboard)
{
    int n;
    vector<int> res;
    res.reserve(64);
    for (n = 0; bitboard != 0; n++, bitboard &= (bitboard - 1))
    {
        res.push_back(log2(bitboard & ~(bitboard - 1)));
    }
    return res;
}


vector<int> bits_asm(uint64_t bitboard)
{
    int64_t n(0);
    int res[64];
    asm(
    "bsf %[b], %%rax\n\t"
    "je exit\n\t"
    ".align 16\n"
"loop:\n\t"
    "mov %%eax, (%[r],%[n],4)\n\t"
    "btr %%rax, %[b]\n\t"
    "inc %[n]\n\t"
    "bsf %[b], %%rax\n\t"
    "je loop\n"
"exit:\n\t"
    : /* output */ "=r" (n)
    : /* input */ [n] "r" (n), [r] "r" (res), [b] "r" (bitboard)
    : /* state */ "eax", "cc"
    );
    return vector<int>(res, res + n);
}




class run_timer
{
public:
    run_timer()
    {
    }

    void start()
    {
        times(&f_start);
    }

    void stop()
    {
        times(&f_stop);
    }

    void report(const char *msg)
    {
        printf("%s:\n"
               "Function started at %ld\n"
               "           ended at %ld\n"
               "              spent %ld (%f seconds)\n",
               msg,
               f_start.tms_utime,
               f_stop.tms_utime,
               f_stop.tms_utime - f_start.tms_utime,
               (double)(f_stop.tms_utime - f_start.tms_utime)/(double)sysconf(_SC_CLK_TCK));
    }

    struct tms f_start;
    struct tms f_stop;
};


int main(int argc, char *argv[])
{
    run_timer t;

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits(rand());
    }
    t.stop();
    t.report("bits version");

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits_c(rand());
    }
    t.stop();
    t.report("c version");

    t.start();
    for(int i(0); i < 10000000; ++i)
    {
        bits_asm(rand());
    }
    t.stop();
    t.report("c version");

    return 0;
}

使用以下命令行编译的g++:

c++ -msse4.2 -O2 -o bits -c bits.cpp

尽管您可能认为-msse4.2可能是log2()版本的问题,但我尝试了没有它,log2()仍然比较慢。顺便说一下,我不建议使用这种方法,因为它不可移植。只有基于英特尔的计算机才能理解这些指令。

5

将您的firstBit函数替换为使用BSFBSR指令的内置函数,可以大大提高速度。

在gcc中,它将是__builtin_ffsll__builtin_ctzll

在Visual C+中,使用_BitScanForward_BitScanReverse


嗯...又有一个非常有趣的建议我自己想到了。然而,有一个问题:我正在使用Mac,并且使用clang++编译器进行编译(这是我唯一找到能完全支持Boost和C++11的编译器)。你有什么想法吗?有没有相当的替代品? - Dr.Kameleon
@Dr.Kameleon:clang支持这些gcc名称下的功能:http://comments.gmane.org/gmane.comp.compilers.clang.devel/2074 - Ben Voigt
1
据传说,NSA 不会购买没有这些指令的计算机。 - brian beuning
另一个奇怪的事情:刚刚测试了我的版本,使用__builtin_ffsll来获取第一个位(而不是De Bruijn乘法)-这已经经过测试并且在Clang上工作,并且期望它会快速执行。问题是...它没有。我的版本与以前一样快,而@icepack的版本仍然是最快的。 - Dr.Kameleon
好的,那真的很奇怪。 - Ben Voigt

3
const size_t size = sizeof(U64)*8;
U64 val = 1;

vector<UINT> res;
res.reserve(size);

for ( size_t i = size; i > 0; --i ) {
  if ( ( val & bitboard ) != 0 ) {
    res.push_back(i);
  }
  val <<= 1;
}

做得很好。但是如果你喜欢的话,循环可以改成 for(...i = size; i--;) 或者 --i - David G
1
非常感谢 - 它确实有效,但速度并没有更快... :S - Dr.Kameleon
@Dr.Kameleon:我修改了代码:有时候内存重新分配占用了大部分时间。 - Naszta

3
我尝试了一个简单的版本,速度快了2-3倍,但首先保留了向量。将保留应用于原始算法后,它击败了简单的算法。
因此,我怀疑向量操作是更大的开销,而不是位操作(甚至是在下一个位函数中使用的乘法)。
还有一些其他加速可以得到,关于查找最低设置位。我的本地log2版本很差,原始帖子中提供的函数也不是非常便宜。
这是我最好的尝试:
void bits(uint64 bitboard, vector<int> &res)
{
    res.resize(64);
    int i = 0;
    while (bitboard)
    {
        int first;
        _BitScanForward64((DWORD *)&first, bitboard);
        res[i++] = first;
        bitboard &= ~(1ULL<<first);
    }
    res.resize(i);
}

使用asm内置函数替换了firstBit函数。使用内置函数可以很好地提升性能。(显然,这段代码并不具备可移植性,但我猜测GCC的变体应该不会太难实现)。

此外,假设向量是相对持久的,而不是一直在动态分配/复制,并且只需根据需要调整其大小。


我尝试了在几乎所有版本中“保留”向量,但效果不明显...如果我们把向量放在一边,你有什么建议? - Dr.Kameleon
有趣。这里会产生很大的影响。 - JasonD
完全摆脱向量,每当您迭代向量时进行位板迭代,可能会带来更大的收益。 - Ben Voigt
是的,如果我使用传统的数组而不是向量,我可以将一部分合理的代码优化掉。 - JasonD

3

目前我能想到最快的方法是使用预生成的map数组,其中包含所有数字(它不必是所有数字,例如您可以将数字分成8位或16位部分,然后使用一些适当的加法将返回的数组连接起来以解决位的实际位置问题)。


我无法想象串联实际上比简单的位操作更快。即使是表查找的缓存命中速度也会更慢。 - Ben Voigt
@BenVoigt 嗯,我想到使用映射表可能不是一个好建议,但是也可以使用数组来完成。 - Luchian Grigore
查找表仍然可能具有较差的性能,因为它占用了缓存空间。位移操作是廉价的。 - Ben Voigt

2

我认为最快、最简单的方法是循环遍历,但如果我们传入一个向量而不是后来再复制,速度应该会更快一些。

void DQBitboard::bits(U64 bitboard, vector<UINT> &res)
{
    res.clear();   // Make sure vector is empty. Not necessary if caller does this!
    int bit = 0;
    while (bitboard)
    {
        if (bitboard & 1) 
            res.push_back(bit);
        bit++;
        bitboard >>= 1;
    }

    return res;
}

在findfirst中的乘法运算会花费一些时间,因此我怀疑它是否真的值得这样做。

如果位棋盘不经常是全零的,那么Ben的变体会更有效一些。 - Mats Petersson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接