【问题标题】:Bit count : preprocessor magic vs modern C++位数:预处理器魔术与现代 C++
【发布时间】:2017-12-24 14:53:36
【问题描述】:

假设我想为 16 位块中的 64 位整数创建一个编译时构造的位计数查找表。我知道这样做的唯一方法是以下代码:

#define B4(n) n, n + 1, n + 1, n + 2
#define B6(n)   B4(n),   B4(n + 1),   B4(n + 1),  B4(n + 2)  
#define B8(n)   B6(n),   B6(n + 1),   B6(n + 1),  B6(n + 2)
#define B10(n)  B8(n),   B8(n + 1),   B8(n + 1),  B8(n + 2)
#define B12(n)  B10(n),  B10(n + 1),  B10(n + 1), B10(n + 2)
#define B14(n)  B12(n),  B12(n + 1),  B12(n + 1), B12(n + 2)
#define B16(n)  B14(n),  B14(n + 1),  B14(n + 1), B14(n + 2)
#define COUNT_BITS B16(0), B16(1), B16(1), B16(2)

unsigned int lookup[65536] = { COUNT_BITS };

是否有现代 (C++11/14) 方法可以获得相同的结果?

【问题讨论】:

  • 您没有足够的内存用于 64 位查找表
  • @Lưu Vĩnh Phúc 我的意思是,可以计算 64 位整数的位数,将它们分成 16 位块并对结果求和。这是一个让你节省空间复杂度的技巧
  • @LưuVĩnhPhúc:再次阅读问题。查找表大小为 65536。数字将按 16 位块进行处理。这里没有人谈论 64 位查找表。
  • 您真的需要查找表吗?还是fast 例程就足够了?在后一种情况下,请参阅问题How to count the number of set bits in a 32-bit integer?Matt Howellsanswer
  • 不管怎样,如果目标处理器支持,实现__builtin_popcount 的x86 编译器将发出popcnt 指令,它们将退回到快速并行Matt Howells 在@CiaPan 链接的答案中提出的位计数算法。所以从来没有真正的理由自己编写该算法,除非您使用的编译器没有内置的人口计数。显然,同样的优化也适用于 std::bitset.count,至少在 Richard Hodges 测试的编译器中是这样。

标签: c++ c++11 bit-manipulation c-preprocessor c++14


【解决方案1】:

为什么不使用标准库?

#include <bitset>

int bits_in(std::uint64_t u)
{
    auto bs = std::bitset<64>(u);
    return bs.count();
}

生成的汇编程序(使用-O2 -march=native 编译):

bits_in(unsigned long):
        xor     eax, eax
        popcnt  rax, rdi
        ret

此时值得一提的是,并非所有 x86 处理器都有此指令,因此(至少使用 gcc)您需要让它知道要编译的架构。

@tambre 提到,实际上,如果可以,优化器会走得更远:

volatile int results[3];

int main()
{
    results[0] = bits_in(255);
    results[1] = bits_in(1023);
    results[2] = bits_in(0x8000800080008000);   
}

生成的汇编器:

main:
        mov     DWORD PTR results[rip], 8
        xor     eax, eax
        mov     DWORD PTR results[rip+4], 10
        mov     DWORD PTR results[rip+8], 4
        ret

像我这样的老派小玩意儿需要找到新的问题来解决:)

更新

并不是每个人都对解决方案依赖 cpu 帮助来计算位数感到高兴。那么如果我们使用自动生成的表格但允许开发人员配置它的大小呢? (警告 - 16 位表版本的编译时间长)

#include <utility>
#include <cstdint>
#include <array>
#include <numeric>
#include <bitset>


template<std::size_t word_size, std::size_t...Is>
constexpr auto generate(std::integral_constant<std::size_t, word_size>, std::index_sequence<Is...>) {
    struct popcount_type {
        constexpr auto operator()(int i) const {
            int bits = 0;
            while (i) {
                i &= i - 1;
                ++bits;
            }
            return bits;
        }
    };
    constexpr auto popcnt = popcount_type();

    return std::array<int, sizeof...(Is)>
            {
                    {popcnt(Is)...}
            };
}

template<class T>
constexpr auto power2(T x) {
    T result = 1;
    for (T i = 0; i < x; ++i)
        result *= 2;
    return result;
}


template<class TableWord>
struct table {
    static constexpr auto word_size = std::numeric_limits<TableWord>::digits;
    static constexpr auto table_length = power2(word_size);
    using array_type = std::array<int, table_length>;
    static const array_type& get_data() {
        static const array_type data = generate(std::integral_constant<std::size_t, word_size>(),
                                           std::make_index_sequence<table_length>());
        return data;
    };

};

template<class Word>
struct use_table_word {
};

template<class Word, class TableWord = std::uint8_t>
int bits_in(Word val, use_table_word<TableWord> = use_table_word<TableWord>()) {
    constexpr auto table_word_size = std::numeric_limits<TableWord>::digits;

    constexpr auto word_size = std::numeric_limits<Word>::digits;
    constexpr auto times = word_size / table_word_size;
    static_assert(times > 0, "incompatible");

    auto reduce = [val](auto times) {
        return (val >> (table_word_size * times)) & (power2(table_word_size) - 1);
    };

    auto const& data = table<TableWord>::get_data();
    auto result = 0;
    for (int i = 0; i < times; ++i) {
        result += data[reduce(i)];
    }
    return result;
}

volatile int results[3];

#include <iostream>

int main() {
    auto input = std::uint64_t(1023);
    results[0] = bits_in(input);
    results[0] = bits_in(input, use_table_word<std::uint16_t>());

    results[1] = bits_in(0x8000800080008000);
    results[2] = bits_in(34567890);

    for (int i = 0; i < 3; ++i) {
        std::cout << results[i] << std::endl;
    }
    return 0;
}

最终更新

此版本允许使用查找表中的任意位数并支持任何输入类型,即使它小于查找表中的位数。

如果高位为零,它也会短路。

#include <utility>
#include <cstdint>
#include <array>
#include <numeric>
#include <algorithm>

namespace detail {
    template<std::size_t bits, typename = void>
    struct smallest_word;

    template<std::size_t bits>
    struct smallest_word<bits, std::enable_if_t<(bits <= 8)>>
    {
        using type = std::uint8_t;
    };

    template<std::size_t bits>
    struct smallest_word<bits, std::enable_if_t<(bits > 8 and bits <= 16)>>
    {
        using type = std::uint16_t;
    };

    template<std::size_t bits>
    struct smallest_word<bits, std::enable_if_t<(bits > 16 and bits <= 32)>>
    {
        using type = std::uint32_t;
    };

    template<std::size_t bits>
    struct smallest_word<bits, std::enable_if_t<(bits > 32 and bits <= 64)>>
    {
        using type = std::uint64_t;
    };
}

template<std::size_t bits> using smallest_word = typename detail::smallest_word<bits>::type;

template<class WordType, std::size_t bits, std::size_t...Is>
constexpr auto generate(std::index_sequence<Is...>) {

    using word_type = WordType;

    struct popcount_type {
        constexpr auto operator()(word_type i) const {
            int result = 0;
            while (i) {
                i &= i - 1;
                ++result;
            }
            return result;
        }
    };
    constexpr auto popcnt = popcount_type();

    return std::array<word_type, sizeof...(Is)>
            {
                    {popcnt(Is)...}
            };
}

template<class T>
constexpr auto power2(T x) {
    return T(1) << x;
}

template<std::size_t word_size>
struct table {

    static constexpr auto table_length = power2(word_size);

    using word_type = smallest_word<word_size>;

    using array_type = std::array<word_type, table_length>;

    static const array_type& get_data() {
        static const array_type data = generate<word_type, word_size>(std::make_index_sequence<table_length>());
        return data;
    };

    template<class Type, std::size_t bits>
    static constexpr auto n_bits() {
        auto result = Type();
        auto b = bits;
        while(b--) {
            result = (result << 1) | Type(1);
        }
        return result;
    };

    template<class Uint>
    int operator()(Uint i) const {
        constexpr auto mask = n_bits<Uint, word_size>();
        return get_data()[i & mask];
    }

};

template<int bits>
struct use_bits {
    static constexpr auto digits = bits;
};

template<class T>
constexpr auto minimum(T x, T y) { return x < y ? x : y; }

template<class Word, class UseBits = use_bits<8>>
int bits_in(Word val, UseBits = UseBits()) {

    using word_type = std::make_unsigned_t<Word>;
    auto uval = static_cast<word_type>(val);


    constexpr auto table_word_size = UseBits::digits;
    constexpr auto word_size = std::numeric_limits<word_type>::digits;

    auto const& mytable = table<table_word_size>();
    int result = 0;
    while (uval)
    {
        result += mytable(uval);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wshift-count-overflow"
                uval >>= minimum(table_word_size, word_size);
#pragma clang diagnostic pop
    }

    return result;
}

volatile int results[4];

#include <iostream>

int main() {
    auto input = std::uint8_t(127);
    results[0] = bits_in(input);
    results[1] = bits_in(input, use_bits<4>());
    results[2] = bits_in(input, use_bits<11>());
    results[3] = bits_in(input, use_bits<15>());

    for (auto&& i : results) {
        std::cout << i << std::endl;
    }

    auto input2 = 0xabcdef;
    results[0] = bits_in(input2);
    results[1] = bits_in(input2, use_bits<4>());
    results[2] = bits_in(input2, use_bits<11>());
    results[3] = bits_in(input2, use_bits<15>());

    for (auto&& i : results) {
        std::cout << i << std::endl;
    }

    auto input3 = -1;
    results[0] = bits_in(input3);
    results[1] = bits_in(input3, use_bits<4>());
    results[2] = bits_in(input3, use_bits<11>());
    results[3] = bits_in(input3, use_bits<15>());

    for (auto&& i : results) {
        std::cout << i << std::endl;
    }

    return 0;
}

示例输出:

7
7
7
7
17
17
17
17
32
32
32
32

例如,调用bits_in(int, use_bits&lt;11&gt;()) 的程序集输出变为:

.L16:
        mov     edx, edi
        and     edx, 2047
        movzx   edx, WORD PTR table<11ul>::get_data()::data[rdx+rdx]
        add     eax, edx
        shr     edi, 11
        jne     .L16

这在我看来是合理的。

【讨论】:

  • 这样更好,因为它节省了大量的 CPU 周期和二级缓存
  • 令人惊讶的是优化器能够计算出bits_in 只不过是return __builtin_popcountll(u),而且不仅可以在编译时计算出来。这就是为什么内在函数在可能的情况下比内联汇编好得多。注意:bitset::count 返回size_t
  • 这是一个问题:“假设我想为 16 位块中的 64 位整数创建一个编译时构造的位计数查找表”。这不是这个问题的答案。您可以提及此解决方案作为替代方案,但这不是答案。太糟糕了,这个答案是最受好评的,
  • @geza:StackOverflow 倾向于解决问题而不是按要求回答问题,特别是因为许多问题都存在 X/Y 问题。 OP 更有可能试图找到一种快速计算位数的方法,而不是使用 16 位表方法(以及为什么是 16 位而不是 8 位?)。如果 OP 要澄清他们绝对想使用一张桌子,即使它更慢,那么它会有所不同......而且是一个相当令人惊讶的问题。
  • @geza 问题清楚地询问“是否有现代(C++11/14)方法来获得相同的结果?”这已经在这里得到了回答。即使目标 CPU 没有 popcnt 指令,也可以合理地假设“现代”编译器会将 std::bitset 方法优化到至少与查找表方法相当的东西。最值得注意的是,因为编译器已经知道,these alternatives 中的哪一个最适合特定的目标平台……
【解决方案2】:

为后代提供更多,使用递归解决方案(log(N) 深度)创建查找表。它使用了 constexpr-if 和 constexpr-array-operator[],所以它非常接近 C++17。

#include <array>

template<size_t Target, size_t I = 1>
constexpr auto make_table (std::array<int, I> in = {{ 0 }})
{
  if constexpr (I >= Target)
  {
    return in;
  }
  else
  {
    std::array<int, I * 2> out {{}};
    for (size_t i = 0; i != I; ++i)
    {
      out[i] = in[i];
      out[I + i] = in[i] + 1;
    }
    return make_table<Target> (out);
  }
}

constexpr auto population = make_table<65536> ();

在这里编译:https://godbolt.org/g/RJG1JA

【讨论】:

    【解决方案3】:

    这是一个 C++14 解决方案,基本上是围绕 constexpr 的用法构建的:

    // this struct is a primitive replacement of the std::array that 
    // has no 'constexpr reference operator[]' in C++14 
    template<int N>
    struct lookup_table {
        int table[N];
    
        constexpr int& operator[](size_t i) { return table[i]; }
        constexpr const int& operator[](size_t i) const { return table[i]; }
    };
    
    constexpr int bit_count(int i) { 
        int bits = 0; 
        while (i) { i &= i-1; ++bits; } 
        return bits;
    }
    
    template<int N> 
    constexpr lookup_table<N> generate() {
        lookup_table<N> table = {};
    
        for (int i = 0; i < N; ++i)
            table[i] = bit_count(i);
    
        return table;
    }
    
    template<int I> struct Check {
        Check() { std::cout <<  I << "\n"; }
    };
    
    constexpr auto table = generate<65536>();
    
    int main() {
        // checks that they are evaluated at compile-time 
        Check<table[5]>();
        Check<table[65535]>();
        return 0;
    }
    

    可运行版本:http://ideone.com/zQB86O

    【讨论】:

    • 为什么在const operator[] 重载中,原始(constexpr)返回类型是按引用而不是按值返回有什么特别的原因吗?我相信重载数组下标运算符通常建议为const 变体返回值,以防返回是原始(/内置)类型,但在诸如此类的上下文中我并不精通constexpr .不错的答案!
    • @dfri,谢谢!不,没有特别的原因,它是 std::array 泛型运算符的“副本”,我相信可以更改为值返回。
    【解决方案4】:

    使用,您可以使用constexpr 在编译时构造查找表。使用population count 计算,查找表可以构造如下:

    #include <array>
    #include <cstdint>
    
    template<std::size_t N>
    constexpr std::array<std::uint16_t, N> make_lookup() {
        std::array<std::uint16_t, N> table {};
    
        for(std::size_t i = 0; i < N; ++i) {
            std::uint16_t popcnt = i;
    
            popcnt = popcnt - ((popcnt >> 1) & 0x5555);
            popcnt = (popcnt & 0x3333) + ((popcnt >> 2) & 0x3333);
            popcnt = ((popcnt + (popcnt >> 4)) & 0x0F0F) * 0x0101;
    
            table[i] = popcnt >> 8;
        }
        return table;
    }
    

    示例用法:

    auto lookup = make_lookup<65536>();
    

    std::array::operator[]constexpr,因为 上面的示例编译但不会是真正的 constexpr


    如果你想惩罚你的编译器,你也可以用可变参数模板初始化生成的std::array。此版本也可以与 一起使用,甚至可以通过使用indices trick 一起使用。

    #include <array>
    #include <cstdint>
    #include <utility>
    
    namespace detail {
    constexpr std::uint8_t popcnt_8(std::uint8_t i) {
        i = i - ((i >> 1) & 0x55);
        i = (i & 0x33) + ((i >> 2) & 0x33);
        return ((i + (i >> 4)) & 0x0F);
    }
    
    template<std::size_t... I>
    constexpr std::array<std::uint8_t, sizeof...(I)>
    make_lookup_impl(std::index_sequence<I...>) {
        return { popcnt_8(I)... };
    }
    } /* detail */
    
    template<std::size_t N>
    constexpr decltype(auto) make_lookup() {
        return detail::make_lookup_impl(std::make_index_sequence<N>{});
    }
    

    注意:在上面的例子中,我从 16 位整数切换到了 8 位整数。

    Assembly Output

    8 位版本将只为detail::make_lookup_impl 函数制作 256 个模板参数,而不是 65536。后者太多了,会超过模板实例化深度的最大值。如果您有足够多的虚拟内存,您可以使用 GCC 上的-ftemplate-depth=65536 编译器标志来增加此最大值并切换回 16 位整数。

    不管怎样,看看下面的演示,尝试一下 8 位版本如何计算 64 位整数的设置位。

    Live Demo

    【讨论】:

    • 在 C++14 中,std::array::operator[] 不是 constexpr,而且似乎这段代码只会在 C++17 的编译时进行评估。这就是我在示例中没有使用std::array 的原因。
    • @DAle,是的,你是对的。我相应地编辑了我的答案。
    • 您可以通过将 table 设为 C 数组、实现 c++17 的 std::to_array 并返回 to_array(table) 来使其在 c++14 中工作。
    • @Erroneous,这是个好主意,但不幸的是,在这种情况下,它会产生很多模板参数(即 65536),并且会超过模板实例化深度的最大值。可以使用-ftemplate-depth=65536 编译器标志来增加此最大值,但它会对编译时间产生严重的负面影响。
    • @Akira 我在 gcc 7.1.1 上没有遇到任何问题。我使用来自en.cppreference.com/w/cpp/experimental/to_array 的实现并使用-std=c++14 -ftemplate-depth=256 编译。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-14
    • 2011-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多