Estaba trabajando en un proyecto personal recientemente cuando me topé con un problema extraño.
En un ciclo muy cerrado, tengo un número entero con un valor entre 0 y 15. Necesito obtener -1 para los valores 0, 1, 8 y 9 y 1 para los valores 4, 5, 12 y 13.
Me volví a Godbolt para verificar algunas opciones y me sorprendió que pareciera que el compilador no podía optimizar una declaración de cambio de la misma manera que una cadena if.
El enlace está aquí: https://godbolt.org/z/WYVBFl
El codigo es:
const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
int a(int num) {
return lookup[num & 0xF];
}
int b(int num) {
num &= 0xF;
if (num == 0 || num == 1 || num == 8 || num == 9)
return -1;
if (num == 4 || num == 5 || num == 12 || num == 13)
return 1;
return 0;
}
int c(int num) {
num &= 0xF;
switch (num) {
case 0: case 1: case 8: case 9:
return -1;
case 4: case 5: case 12: case 13:
return 1;
default:
return 0;
}
}
Pensé que byc produciría los mismos resultados, y esperaba poder leer los trucos de bits para obtener una implementación eficiente yo mismo, ya que mi solución (la declaración de cambio, en otra forma) fue bastante lenta.
Curiosamente, se b
compiló en bit-hacks mientras no c
estaba optimizado o se redujo a un caso diferente a
dependiendo del hardware de destino.
¿Alguien puede explicar por qué existe esta discrepancia? ¿Cuál es la forma 'correcta' de optimizar esta consulta?
EDITAR:
Aclaración
Yo quiero la solución cambie a ser el más rápido, o una solución similar "limpia". Sin embargo, cuando se compila con optimizaciones en mi máquina, la solución if es significativamente más rápida.
Escribí un programa rápido para demostrar y TIO tiene los mismos resultados que encuentro localmente: ¡ Pruébelo en línea!
Con static inline
la tabla de búsqueda se acelera un poco: ¡ Pruébelo en línea!
fuente
-O3
, y compilóc
algo probablemente peora
ob
(c
tenía dos saltos condicionales más algunas manipulaciones de bits, frente a un solo salto condicional y una manipulación de bits más simpleb
), pero aún así mejor que las pruebas ingenuas ítem por ítem. No estoy seguro de lo que realmente estás pidiendo aquí; El simple hecho es que un compilador optimizador puede convertir cualquiera de estos en cualquiera de los otros si así lo desea, y no hay reglas estrictas para lo que hará o no hará.if
aún lateswitch
(la búsqueda extraña se vuelve aún más rápida) [TIO a seguir]Respuestas:
Si enumera explícitamente todos los casos, gcc es muy eficiente:
solo se compila en una rama indizada simple:
Tenga en cuenta que si no
default:
está comentado, gcc vuelve a su versión de rama anidada.fuente
pslld
/psrad
o sus equivalentes AVX2 de 8 vías. Mucho depende de las otras particularidades de su código.Los compiladores de C tienen casos especiales
switch
, porque esperan que los programadores entiendan el idiomaswitch
y lo exploten.Código como:
no pasaría la revisión de los codificadores C competentes tres o cuatro revisores exclamarían simultáneamente "¡esto debería ser un
switch
!"No vale la pena que los compiladores de C analicen la estructura de las
if
declaraciones para la conversión a una tabla de salto. Las condiciones para eso tienen que ser correctas, y la cantidad de variación que es posible en un montón deif
declaraciones es astronómica. El análisis es complicado y es probable que resulte negativo (como en "no, no podemos convertir estosif
s en aswitch
").fuente
if
si es posible.static
y use inicializadores designados C99 si desea dejar un poco más claro lo que está asignando, y está claramente perfectamente bien.if
(ver edición). @R .. Desarrollé la solución bit a bit completa para el compilador, que es lo que estoy usando por ahora. Desafortunadamente en mi caso, estos sonenum
valores, no enteros desnudos, por lo que los hacks bit a bit no son muy fáciles de mantener.El siguiente código calculará su búsqueda sin bifurcación, sin LUT, en ~ 3 ciclos de reloj, ~ 4 instrucciones útiles y ~ 13 bytes de
inline
código de máquina x86 de alta capacidad .Depende de la representación entera del complemento a 2.
Sin embargo, debe asegurarse de que los
u32
ys32
typedefs realmente apunten a tipos enteros sin signo y con signo de 32 bits.stdint.h
tiposuint32_t
yint32_t
habría sido adecuado, pero no tengo idea si el encabezado está disponible para usted.Véalo usted mismo aquí: https://godbolt.org/z/AcJWWf
Sobre la selección de la constante
Su búsqueda es de 16 constantes muy pequeñas entre -1 y +1 inclusive. Cada uno cabe dentro de 2 bits y hay 16 de ellos, que podemos presentar de la siguiente manera:
Al colocarlos con el índice 0 más cercano al bit más significativo, un solo desplazamiento de
2*num
colocará el bit de signo de su número de 2 bits en el bit de signo del registro. Desplazar a la derecha el número de 2 bits por 32-2 = signo de 30 bits, lo extiende al completoint
, completando el truco.fuente
magic
comentario que explica cómo regenerarlo. ¿Podría explicar cómo se le ocurrió?!!(12336 & (1<<x))-!!(771 & (1<<x));
Puede crear el mismo efecto usando solo aritmética:
Aunque, técnicamente, esta sigue siendo una búsqueda (bit a bit).
Si lo anterior parece demasiado arcano, también puedes hacer:
fuente