c++ c++11 bit-manipulation

¿Cómo escribo una máscara de bits de tiempo de compilación rápida y fácil de mantener en C++?



c++11 bit-manipulation (6)

Tengo un código que es más o menos así:

#include <bitset> enum Flags { A = 1, B = 2, C = 3, D = 5, E = 8, F = 13, G = 21, H, I, J, K, L, M, N, O }; void apply_known_mask(std::bitset<64> &bits) { const Flags important_bits[] = { B, D, E, H, K, M, L, O }; std::remove_reference<decltype(bits)>::type mask{}; for (const auto& bit : important_bits) { mask.set(bit); } bits &= mask; }

Clang> = 3.6 hace lo inteligente y lo compila en una sola instrucción and instrucción (que luego se inserta en cualquier otra parte):

apply_known_mask(std::bitset<64ul>&): # @apply_known_mask(std::bitset<64ul>&) and qword ptr [rdi], 775946532 ret

Pero cada versión de GCC que he intentado compila esto en un enorme desorden que incluye el manejo de errores que debería ser estáticamente DCE. En otro código, ¡incluso colocará los equivalentes important_bits como datos en línea con el código!

.LC0: .string "bitset::set" .LC1: .string "%s: __position (which is %zu) >= _Nb (which is %zu)" apply_known_mask(std::bitset<64ul>&): sub rsp, 40 xor esi, esi mov ecx, 2 movabs rax, 21474836482 mov QWORD PTR [rsp], rax mov r8d, 1 movabs rax, 94489280520 mov QWORD PTR [rsp+8], rax movabs rax, 115964117017 mov QWORD PTR [rsp+16], rax movabs rax, 124554051610 mov QWORD PTR [rsp+24], rax mov rax, rsp jmp .L2 .L3: mov edx, DWORD PTR [rax] mov rcx, rdx cmp edx, 63 ja .L7 .L2: mov rdx, r8 add rax, 4 sal rdx, cl lea rcx, [rsp+32] or rsi, rdx cmp rax, rcx jne .L3 and QWORD PTR [rdi], rsi add rsp, 40 ret .L7: mov ecx, 64 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:.LC1 xor eax, eax call std::__throw_out_of_range_fmt(char const*, ...)

¿Cómo debo escribir este código para que ambos compiladores puedan hacer lo correcto? En su defecto, ¿cómo debo escribir esto para que quede claro, rápido y fácil de mantener?


Aquí hay algunas ideas "inteligentes" que están muy lejos. Probablemente no estés ayudando a la mantenibilidad al seguirlos

es

{B, D, E, H, K, M, L, O};

mucho más fácil de escribir que

(B| D| E| H| K| M| L| O);

?

Entonces no se necesita nada del resto del código.


Desde C ++ 11 también puedes usar la técnica TMP clásica:

template<std::uint64_t Flag, std::uint64_t... Flags> struct bitmask { static constexpr std::uint64_t mask = bitmask<Flag>::value | bitmask<Flags...>::value; }; template<std::uint64_t Flag> struct bitmask<Flag> { static constexpr std::uint64_t value = (uint64_t)1 << Flag; }; void apply_known_mask(std::bitset<64> &bits) { constexpr auto mask = bitmask<B, D, E, H, K, M, L, O>::value; bits &= mask; }

Enlace a Compiler Explorer: https://godbolt.org/z/Gk6KX1

La ventaja de este enfoque sobre la función constexpr de la plantilla es que es potencialmente un poco más rápido de compilar debido a la regla de Chiel .


La mejor versión es c ++ 17 :

template< unsigned char... indexes > constexpr unsigned long long mask(){ return ((1ull<<indexes)|...|0ull); }

Entonces

void apply_known_mask(std::bitset<64> &bits) { constexpr auto m = mask<B,D,E,H,K,M,L,O>(); bits &= m; }

De vuelta en c ++ 14 , podemos hacer este extraño truco:

template< unsigned char... indexes > constexpr unsigned long long mask(){ auto r = 0ull; using discard_t = int[]; // data never used // value never used: discard_t discard = {0,(void( r |= (1ull << indexes) // side effect, used ),0)...}; (void)discard; // block unused var warnings return r; }

o, si estamos atascados con c ++ 11 , podemos resolverlo recursivamente:

constexpr unsigned long long mask(){ return 0; } template<class...Tail> constexpr unsigned long long mask(unsigned char b0, Tail...tail){ return (1ull<<b0) | mask(tail...); } template< unsigned char... indexes > constexpr unsigned long long mask(){ return mask(indexes...); }

Godbolt con los 3 : puede cambiar CPP_VERSION define y obtener el ensamblaje idéntico.

En la práctica usaría lo más moderno que pude. 14 tiempos 11 porque no tenemos recursión y, por lo tanto, la longitud del símbolo O (n ^ 2) (que puede explotar el tiempo de compilación y el uso de la memoria del compilador); 17 beats 14 porque el compilador no tiene que eliminar el array de código muerto, y ese truco de array es feo.

De estos 14 es el más confuso. Aquí creamos una matriz anónima de todos los 0, mientras que como efecto secundario construimos nuestro resultado y luego descartamos la matriz. La matriz descartada tiene un número de 0s igual al tamaño de nuestro paquete, más 1 (que agregamos para que podamos manejar paquetes vacíos).

Una explicación detallada de lo que está haciendo la versión c ++ 14 . Este es un truco / truco, y el hecho de que tenga que hacer esto para expandir los paquetes de parámetros con eficiencia en C ++ 14 es una de las razones por las que las expresiones de plegado se agregaron en c ++ 17 .

Se entiende mejor desde adentro hacia afuera:

r |= (1ull << indexes) // side effect, used

esto solo actualiza r con 1<<indexes para un índice fijo. indexes son un paquete de parámetros, así que tendremos que expandirlo.

El resto del trabajo es proporcionar un paquete de parámetros para expandir los indexes dentro de.

Un paso hacia fuera:

(void( r |= (1ull << indexes) // side effect, used ),0)

aquí dejamos nuestra expresión en void , lo que indica que no nos importa su valor de retorno (solo queremos el efecto secundario de configurar r - en C ++, las expresiones como a |= b también devuelven el valor al que establecen a).

Luego usamos el operador de coma , y 0 para descartar el "valor" y devolver el valor 0 . Así que esta es una expresión cuyo valor es 0 y como efecto secundario del cálculo de 0 establece un bit en r .

int discard[] = {0,(void( r |= (1ull << indexes) // side effect, used ),0)...};

En este punto, expandimos los indexes paquete de parámetros. Así obtenemos:

{ 0, (expression that sets a bit and returns 0), (expression that sets a bit and returns 0), [...] (expression that sets a bit and returns 0), }

en el {} . Este uso de , no es el operador de coma, sino el separador de elementos de matriz. Esto es sizeof...(indexes)+1 0 s, que también establece bits en r como efecto secundario. Luego, asignamos las instrucciones de construcción de la matriz {} a un discard matriz.

A continuación, emitimos discard a void : la mayoría de los compiladores le avisarán si crea una variable y nunca la lee. Todos los compiladores no se quejarán si lo void , es una forma de decir "Sí, lo sé, no uso esto", por lo que suprime la advertencia.


La optimización que está buscando parece ser el peeling de bucle, que se habilita en -O3 , o manualmente con -fpeel-loops . No estoy seguro de por qué esto está incluido en el ámbito del peeling de bucle en lugar del desenrollado del bucle, pero posiblemente no esté dispuesto a desenrollar un bucle con un flujo de control no local dentro de él (ya que, potencialmente, hay un control de rango).

Sin embargo, de forma predeterminada, GCC no llega a poder pelar todas las iteraciones, lo que aparentemente es necesario. Experimentalmente, pasar -O2 -fpeel-loops --param max-peeled-insns=200 (el valor predeterminado es 100) hace el trabajo con su código original: https://godbolt.org/z/NNWrga


Le animo a escribir un tipo de EnumSet adecuado.

Escribir un EnumSet<E> básico EnumSet<E> en C ++ 14 (en adelante) basado en std::uint64_t es trivial:

template <typename E> class EnumSet { public: constexpr EnumSet() = default; constexpr EnumSet(std::initializer_list<E> values) { for (auto e : values) { set(e); } } constexpr bool has(E e) const { return mData & mask(e); } constexpr EnumSet& set(E e) { mData |= mask(e); return *this; } constexpr EnumSet& unset(E e) { mData &= ~mask(e); return *this; } constexpr EnumSet& operator&=(const EnumSet& other) { mData &= other.mData; return *this; } constexpr EnumSet& operator|=(const EnumSet& other) { mData |= other.mData; return *this; } private: static constexpr std::uint64_t mask(E e) { return std::uint64_t(1) << e; } std::uint64_t mData = 0; };

Esto le permite escribir código simple:

void apply_known_mask(EnumSet<Flags>& flags) { static constexpr EnumSet<Flags> IMPORTANT{ B, D, E, H, K, M, L, O }; flags &= IMPORTANT; }

En C ++ 11, requiere algunas circunvoluciones, pero sigue siendo posible:

template <typename E> class EnumSet { public: template <E... Values> static constexpr EnumSet make() { return EnumSet(make_impl(Values...)); } constexpr EnumSet() = default; constexpr bool has(E e) const { return mData & mask(e); } void set(E e) { mData |= mask(e); } void unset(E e) { mData &= ~mask(e); } EnumSet& operator&=(const EnumSet& other) { mData &= other.mData; return *this; } EnumSet& operator|=(const EnumSet& other) { mData |= other.mData; return *this; } private: static constexpr std::uint64_t mask(E e) { return std::uint64_t(1) << e; } static constexpr std::uint64_t make_impl() { return 0; } template <typename... Tail> static constexpr std::uint64_t make_impl(E head, Tail... tail) { return mask(head) | make_impl(tail...); } explicit constexpr EnumSet(std::uint64_t data): mData(data) {} std::uint64_t mData = 0; };

Y se invoca con:

void apply_known_mask(EnumSet<Flags>& flags) { static constexpr EnumSet<Flags> IMPORTANT = EnumSet<Flags>::make<B, D, E, H, K, M, L, O>(); flags &= IMPORTANT; }

Incluso GCC genera de forma trivial and una instrucción en -O1 godbolt :

apply_known_mask(EnumSet<Flags>&): and QWORD PTR [rdi], 775946532 ret


si usar solo C ++ 11 es una necesidad (&a)[N] es una forma de capturar arreglos. Esto le permite escribir una sola función recursiva sin utilizar las funciones de ayuda en absoluto:

template <std::size_t N> constexpr std::uint64_t generate_mask(Flags const (&a)[N], std::size_t i = 0u){ return i < N ? (1ull << a[i] | generate_mask(a, i + 1u)) : 0ull; }

asignándolo a un constexpr auto :

void apply_known_mask(std::bitset<64>& bits) { constexpr const Flags important_bits[] = { B, D, E, H, K, M, L, O }; constexpr auto m = generate_mask(important_bits); //< here bits &= m; }

Prueba

int main() { std::bitset<64> b; b.flip(); apply_known_mask(b); std::cout << b.to_string() << ''/n''; }

Salida

0000000000000000000000000000000000101110010000000000000100100100 // ^ ^^^ ^ ^ ^ ^ // O MLK H E D B

Uno realmente tiene que apreciar la capacidad de C ++ para calcular cualquier cosa computable en el momento de la compilación. Seguramente todavía sopla mi mente ( <> ).

Para las versiones posteriores, yakk''s respuesta de yakk''s C ++ 14 y C ++ 17 ya cubre eso maravillosamente.