web-dev-qa-db-fra.com

Nombre de bits: magie du préprocesseur vs C ++ moderne

Supposons que je veuille créer une table de recherche de comptage de bits construite en temps de compilation pour des entiers 64 bits en blocs de 16 bits. La seule façon que je sache de le faire est le code suivant:

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

Existe-t-il une manière moderne (C++ 11/14) d'obtenir le même résultat?

38
Giorgio Gambino

Pourquoi ne pas utiliser la bibliothèque standard?

#include <bitset>

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

assembleur résultant (compilé avec -O2 -march=native):

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

Il convient de mentionner à ce stade que tous les processeurs x86 n'ont pas cette instruction, donc (au moins avec gcc), vous devrez lui faire savoir pour quelle architecture compiler.

@tambre a mentionné qu'en réalité, quand il le peut, l'optimiseur ira plus loin:

volatile int results[3];

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

assembleur résultant:

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

Les bit-twiddlers de la vieille école comme moi doivent trouver de nouveaux problèmes à résoudre :)

Mise à jour

Tout le monde n'était pas content que la solution repose sur l'aide du processeur pour calculer le bitcount. Et si nous utilisions une table générée automatiquement mais permettions au développeur d'en configurer la taille? (avertissement - temps de compilation long pour la version table 16 bits)

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

Mise à jour finale

Cette version permet d'utiliser n'importe quel nombre de bits dans la table de recherche et prend en charge tout type d'entrée, même s'il est plus petit que le nombre de bits dans la table de recherche.

Il court-circuite également si les bits hauts sont nuls.

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

exemple de sortie:

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

La sortie Assembly résultante pour un appel à bits_in(int, use_bits<11>()) par exemple, devient:

.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

Ce qui me semble raisonnable.

85
Richard Hodges

Il s'agit d'une solution C++ 14, construite essentiellement autour de l'utilisation de 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;
}

Version exécutable: http://ideone.com/zQB86O

22
DAle

Avec c ++ 17 vous pouvez utiliser constexpr pour construire la table de recherche au moment de la compilation. Avec le calcul dénombrement de la population , la table de recherche peut être construite comme suit:

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

Exemple d'utilisation:

auto lookup = make_lookup<65536>();

Le std::array::operator[] est constexpr puisque c ++ 17 , avec c ++ 14 l'exemple ci-dessus compile mais ne sera pas un vrai constexpr.


Si vous souhaitez punir votre compilateur, vous pouvez initialiser le std::array avec des modèles variadic aussi. Cette version fonctionnera aussi avec c ++ 14 et même avec c ++ 11 en utilisant astuce indices .

#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>{});
}

Remarque: Dans l'exemple ci-dessus, je suis passé aux entiers 8 bits à partir d'entiers 16 bits.

Assembly Output

La version 8 bits ne fera que 256 arguments de modèle pour detail::make_lookup_impl fonction au lieu de 65536. Cette dernière est trop importante et dépassera la profondeur maximale d'instanciation du modèle. Si vous avez plus que suffisamment de mémoire virtuelle, vous pouvez augmenter ce maximum avec -ftemplate-depth=65536 drapeau du compilateur sur GCC et revenir aux entiers 16 bits.

Quoi qu'il en soit, jetez un œil à la démo suivante et essayez comment la version 8 bits compte les bits définis d'un entier 64 bits.

Live Demo

20
Akira

Un de plus pour la postérité, en créant une table de correspondance à l'aide d'une solution récursive (de profondeur log (N)). Il utilise constexpr-if et constexpr-array-operator [], il s'agit donc bien de 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> ();

Voir la compilation ici: https://godbolt.org/g/RJG1JA

2
Matt A