【问题标题】:Get an array of the bit positions within a 64-bit integer获取 64 位整数内的位位置数组
【发布时间】:2012-12-30 21:55:24
【问题描述】:

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

  • 例如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;
}

而且代码肯定确实有效。

我的观点是:

  • 您有什么更快的实施方案吗?
  • 您注意到有什么可以优化的吗?如果有,是什么?

提示:

  • UINT 是 unsigned int 的类型定义
  • U64 是 unsigned long long 的类型定义
  • 这两种方法都是static inline。

【问题讨论】:

  • @K-ballo Chess Engine 编程和使用位板(64 位无符号整数)进行棋盘表示和移动生成。 :-)
  • 您的 find first set bit 看起来不错,但我相信我们可以想出一个更好的方法来获得一个单词中的所有位。例如 Luchian 所描述的。
  • 您的示例是否正确 - 您似乎返回了奇数位,这些位位于偶数位位置 0、2、4 ...
  • 您希望设置多少位?可能存在一个阈值,低于该阈值查找表方法更快,高于该阈值时,朴素迭代更快。

标签: c++ c 64-bit bit-manipulation bits


【解决方案1】:

这是另一个可以分析的建议(可以与其他建议结合以进行进一步优化)。注意,这里的循环是O(number of set bits)。

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%……印象深刻。
  • 我将选择这个作为答案 - 因为它是迄今为止我的案例中最快的。但是,我仍然愿意接受新的建议。再次感谢,伙计! :-)
  • log2 是问题中的 debruijn64 查找吗?或者更快的实现?
  • @Dr.Kameleon:我知道行为是什么,我不知道实现是什么(这将决定或破坏该方法的性能)。
  • @BenVoigt 这是个好问题。由用户决定 log2 的实现将是什么以及它是否足够好 - 该算法不限于任何特定的实现;这就是为什么我写了“这个建议可以被分析”。还应该注意的是,算法的性能与输入中设置的位数直接相关,对于所有设置的位数,它不会比简单的迭代做得更好
【解决方案2】:

位移真的很便宜。查找表需要缓存空间,并且您的查找也有整数乘法。我预计,蛮力会比聪明的技术更快。

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 是迄今为止最昂贵的部分。考虑直接使用位板。例如:

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;

我想你会得到你的比特列表。

版本 2:

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;
    }
};

【讨论】:

  • 非常感谢所有的努力。刚刚也测试了你的版本,你就知道了:令人惊讶的是,你的版本(肯定看起来不错而且很有前途)比我的慢了大约 10%……哈哈。我不知道为什么——可能是因为那些 U64 数字通常设置的位很少?
  • @Dr.Kameleon:您是单独对它进行基准测试,还是在您的整个程序中进行基准测试?缓存空间的成本不会显示在微基准测试中,只有在存在缓存压力时才会显示。
  • 这个功能是我的一个更大的国际象棋引擎项目的一部分,我还编写了一个附加的小型基准测试套件,以便我可以测量执行的关键功能的时间/性能等几百万次...
  • @Dr.Kameleon:对。但是,您是在对实际国际象棋引擎中运行的循环进行基准测试,还是对仅包含此函数的循环进行基准测试?在微基准测试中,查找表很容易放入缓存中。在您的真实代码中,查找表可能会被淘汰,以支持您正在处理的其他数据。
  • @Dr.Kameleon:看看那个迭代器是否适合你?当然,您也可以使用 DeBruijn 查找来代替。
【解决方案3】:

我一直想知道使用 bst 汇编指令是否会更快。于是我尝试了 3 次实现,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() 速度较慢。

顺便说一句,我不推荐这种方法,因为它不可移植。只有基于 Intel 的计算机才能理解这些指令。

【讨论】:

    【解决方案4】:

    使用 BSF 或 BSR 指令将您的 firstBit 函数替换为内部函数,以实现大幅加速。

    在 gcc 中,应该是 __builtin_ffsll 和 __builtin_ctzll

    使用 Visual C+、_BitScanForward 和 _BitScanReverse

    【讨论】:

    • 嗯...我自己想到的另一个非常有趣的建议。但是有一个问题:我正在使用 Mac 并使用 clang++ 编译器进行编译(唯一一个完全支持 Boost 和 C++11 的编译器)。有任何想法吗?有没有等价物?
    • @Dr.Kameleon:clang 在 gcc 名称下支持这些:comments.gmane.org/gmane.comp.compilers.clang.devel/2074
    • 根据传说,美国国家安全局不会购买没有这些说明的计算机。
    • 另一个奇怪的事情:刚刚测试了 my 版本,使用 __builtin_ffsll 获取第一位(而不是 De Bruijn 乘法) - 已经过测试并使用 Clang -并期待它走得很快。问题是……它没有。我的版本和以前差不多,@icepack 的版本仍然是最快的。
    • 好吧,这很奇怪。
    【解决方案5】:

    我现在能想到的最快的方法是使用预先生成的所有数字的 map 数组(不一定是所有数字,例如,您可以打破数字在 8 位或 16 位部分中,然后将返回的数组与一些适当的添加连接起来,以说明位的实际位置)。

    【讨论】:

    • 我无法想象串联实际上比简单的位旋转更快。即使是表查找的缓存命中也会变慢。
    • @BenVoigt 是的,我认为地图不是一个好建议,但也可以使用数组来完成。
    • 查找表仍然可能具有更差的性能,因为它占用了缓存空间。位移很便宜。
    【解决方案6】:

    我尝试了一个简单的版本,它的时钟速度快了大约 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 变体应该不会太棘手)。

    还假设向量是相当持久的,而不是一直被动态分配/复制,并且只是适当地调整它的大小。

    【讨论】:

    • 我在几乎所有版本中都尝试过“保留”向量,如果有的话效果并不明显...... - 如果我们把向量放在一边,你有什么建议?
    • 有趣。在这里有很大的不同。
    • 完全摆脱向量,并在迭代向量时进行位板迭代,可能是一个更大的胜利。
    • 是的,我可以通过移动到老式数组而不是向量来取出合理的块。
    【解决方案7】:
    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。
    • 非常感谢 - 它确实有效,但速度并不快......:S
    • @Dr.Kameleon:我修改了代码:有时内存重新分配占用了大部分时间。
    【解决方案8】:

    我实际上认为最快、最简单的方法就是简单地循环,但是如果我们传入一个向量而不是稍后再进行复制,它应该会快一点。

    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 的变体会更有效。
    猜你喜欢
    • 1970-01-01
    • 2015-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-15
    • 2011-02-28
    • 1970-01-01
    相关资源
    最近更新 更多