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?
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 :)
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;
}
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.
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
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.
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.
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