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 and
instrucción (que luego se inserta en cualquier otro lugar):
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 probado compila esto en un lío enorme que incluye el manejo de errores que debería ser DCE estáticamente. En otro código, ¡incluso colocará el important_bits
equivalente 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? De no ser así, ¿cómo debería escribir esto para que quede claro, rápido y fácil de mantener?
c++
c++11
bit-manipulation
Alex Reinking
fuente
fuente
B | D | E | ... | O
?(1ULL << B) | ... | (1ULL << O)
(1ULL << Constant)
| por línea, y alinee los nombres de las constantes en las diferentes líneas, eso sería más fácil para los ojos.int
resultado de la operación de bits PUEDE SERint
O puedelong long
depender del valor y formalmenteenum
no es equivalente a unaint
constante. clang pide "como si", gcc sigue siendo pedanteRespuestas:
La mejor versión es c ++ 17:
Luego
de nuevo en c ++ 14, podemos hacer este extraño truco:
o, si nos quedamos con c ++ 11, podemos resolverlo de forma recursiva:
Godbolt con los 3 : puede cambiar la definición de CPP_VERSION y obtener un ensamblaje idéntico.
En la práctica, usaría lo más moderno que pudiera. 14 vence a 11 porque no tenemos recursividad y, por lo tanto, longitud de símbolo O (n ^ 2) (que puede aumentar el tiempo de compilación y el uso de la memoria del compilador); 17 vence a 14 porque el compilador no tiene que eliminar el código muerto de esa matriz, y ese truco de matriz es simplemente feo.
De estos 14 es el más confuso. Aquí creamos una matriz anónima de todos los 0, mientras tanto, como efecto secundario, construimos nuestro resultado y luego descartamos la matriz. La matriz descartada tiene un número de ceros igual al tamaño de nuestro paquete, más 1 (que agregamos para poder manejar paquetes vacíos).
Una explicación detallada de lo que c ++ 14la versión está haciendo. Esto 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 se agregaron expresiones de plegado enc ++ 17.
Se entiende mejor de adentro hacia afuera:
esto solo se actualiza
r
con1<<indexes
para un índice fijo.indexes
es un paquete de parámetros, así que tendremos que expandirlo.El resto del trabajo es proporcionar un paquete de parámetros para expandir el
indexes
interior.Un paso fuera:
aquí convertimos nuestra expresión a
void
, lo que indica que no nos importa su valor de retorno (solo queremos el efecto secundario de establecerr
: en C ++, las expresiones comoa |= b
también devuelven el valor que establecierona
).Luego usamos el operador de coma
,
y0
para descartar elvoid
"valor", y devolvemos el valor0
. Así que esta es una expresión cuyo valor es0
y, como efecto secundario del cálculo,0
se establece un pocor
.En este punto, expandimos el paquete de parámetros
indexes
. Entonces obtenemos:en el
{}
. Este uso de no,
es el operador de coma, sino el separador de elementos de matriz. Esto es s, que también establece bits como efecto secundario. Luego asignamos las instrucciones de construcción de la matriz a una matriz .sizeof...(indexes)+1
0
r
{}
discard
A continuación, enviamos
discard
avoid
: la mayoría de los compiladores le advertirán si crea una variable y nunca la lee. Todos los compiladores no se quejarán si lo envía avoid
, es una forma de decir "Sí, lo sé, no estoy usando esto", por lo que suprime la advertencia.fuente
((1ull<<indexes)|...|0ull)
, es una "expresión de doblez" . Específicamente, es un "pliegue binario a la derecha" y debe analizarse como(pack
op
...
op
init)
La optimización que está buscando parece ser el peeling de bucle, que está habilitado en
-O3
o manualmente con-fpeel-loops
. No estoy seguro de por qué esto cae dentro del alcance del pelado de bucles en lugar del desenrollado de bucles, pero posiblemente no esté dispuesto a desenrollar un bucle con un flujo de control no local dentro de él (como existe, potencialmente, de la verificación 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/NNWrgafuente
-O3 -fpeel-loops --param max-peeled-insns=200
falla ... Se debe-ftree-slp-vectorize
aparentemente.si usar solo C ++ 11 es imprescindible,
(&a)[N]
es una forma de capturar matrices. Esto le permite escribir una sola función recursiva sin usar funciones auxiliares en absoluto:asignándolo a
constexpr auto
:Prueba
Salida
uno realmente tiene que apreciar la capacidad de C ++ para calcular cualquier cosa que sea computable en tiempo de compilación. Seguramente todavía me sorprende ( <> ).
Para las versiones posteriores, C ++ 14 y C ++ 17, la respuesta de yakk ya cubre maravillosamente eso.
fuente
apply_known_mask
realmente optimiza?constexpr
. Y aunque en teoría eso no es suficiente, sabemos que GCC es bastante capaz de evaluarconstexpr
según lo previsto.Le animo a escribir un
EnumSet
tipo adecuado .Escribir un básico
EnumSet<E>
en C ++ 14 (en adelante) basado enstd::uint64_t
es trivial:Esto le permite escribir código simple:
En C ++ 11, requiere algunas convoluciones, pero sigue siendo posible de todos modos:
Y se invoca con:
Incluso GCC genera trivialmente una
and
instrucción en-O1
godbolt :fuente
constexpr
código no es legal. Quiero decir, ¡algunos tienen 2 declaraciones! (C ++ 11 constexpr apesta)EnumSet<E>
no utiliza un valor deE
como valor directamente, sino que utiliza1 << e
. Es un dominio completamente diferente, que es en realidad lo que hace que la clase sea tan valiosa => sin posibilidad de indexar accidentalmente por ene
lugar de1 << e
.Desde C ++ 11 también puede usar la técnica clásica de TMP:
Enlace al Explorador del compilador: https://godbolt.org/z/Gk6KX1
La ventaja de este enfoque sobre la función de plantilla constexpr es que es potencialmente un poco más rápido de compilar debido a la regla de Chiel .
fuente
Aquí hay algunas ideas demasiado "inteligentes". Probablemente no esté ayudando a la mantenibilidad siguiéndolos.
es
mucho más fácil de escribir que
?
Entonces no se necesita nada del resto del código.
fuente