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 bcompiló en bit-hacks mientras no cestaba optimizado o se redujo a un caso diferente adependiendo 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 inlinela tabla de búsqueda se acelera un poco: ¡ Pruébelo en línea!
fuente

-O3, y compilócalgo probablemente peoraob(ctení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á.ifaú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/psrado 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 idiomaswitchy 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
ifdeclaraciones 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 deifdeclaraciones es astronómica. El análisis es complicado y es probable que resulte negativo (como en "no, no podemos convertir estosifs en aswitch").fuente
ifsi es posible.staticy 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 sonenumvalores, 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
inlinecó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
u32ys32typedefs realmente apunten a tipos enteros sin signo y con signo de 32 bits.stdint.htiposuint32_tyint32_thabrí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*numcolocará 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
magiccomentario 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