Resumen:
Estoy buscando la forma más rápida de calcular
(int) x / (int) y
sin obtener una excepción para y==0
. En cambio, solo quiero un resultado arbitrario.
Antecedentes:
Al codificar algoritmos de procesamiento de imágenes, a menudo necesito dividir por un valor alfa (acumulado). La variante más simple es el código C simple con aritmética de enteros. Mi problema es que normalmente obtengo una división por error cero para los píxeles de resultado con alpha==0
. Sin embargo, estos son exactamente los píxeles en los que el resultado no importa en absoluto: no me importan los valores de color de los píxeles con alpha==0
.
Detalles:
Estoy buscando algo como:
result = (y==0)? 0 : x/y;
o
result = x / MAX( y, 1 );
xey son números enteros positivos. El código se ejecuta una gran cantidad de veces en un bucle anidado, por lo que estoy buscando una forma de deshacerme de la ramificación condicional.
Cuando y no excede el rango de bytes, estoy satisfecho con la solución
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
Pero esto obviamente no funciona bien para rangos más grandes.
Supongo que la pregunta final es: ¿Cuál es el truco más rápido que cambia de 0 a cualquier otro valor entero, dejando todos los demás valores sin cambios?
Aclaraciones
No estoy 100% seguro de que la ramificación sea demasiado cara. Sin embargo, se utilizan diferentes compiladores, por lo que prefiero la evaluación comparativa con pequeñas optimizaciones (lo que de hecho es cuestionable).
Por supuesto, los compiladores son geniales cuando se trata de juegos de bits, pero no puedo expresar el resultado "no me importa" en C, por lo que el compilador nunca podrá usar la gama completa de optimizaciones.
El código debe ser totalmente compatible con C, las principales plataformas son Linux de 64 bits con gcc & clang y MacOS.
fuente
y += !y
¿ Quizás ? No se necesita ninguna rama para calcular eso. Se podría compararx / (y + !y)
conx / max(y, 1)
y quizás tambiény ? (x/y) : 0
. Supongo que no habrá ninguna rama en ninguno de ellos, al menos con las optimizaciones activadas.0
secciones alfa son enormes y contiguas. Hay un lugar para jugar con las micro optimizaciones, y las operaciones por píxel es exactamente ese lugar.Respuestas:
Inspirado por algunos de los comentarios, me deshice de la rama en mi Pentium y el
gcc
compilador usandoEl compilador básicamente reconoce que puede usar un indicador de condición de la prueba en la adición.
Según solicitud de la asamblea:
Como esta resultó ser una pregunta y respuesta tan popular, elaboraré un poco más. El ejemplo anterior se basa en el lenguaje de programación que reconoce un compilador. En el caso anterior, se usa una expresión booleana en aritmética integral y el uso de indicadores de condición se inventa en hardware para este propósito. En general, las banderas solo son accesibles en C mediante el uso de idiom. Es por eso que es tan difícil hacer una biblioteca de enteros de precisión múltiple portátil en C sin recurrir al ensamblaje (en línea). Supongo que la mayoría de los compiladores decentes entenderán el idioma anterior.
Otra forma de evitar las ramas, como también se señaló en algunos de los comentarios anteriores, es la ejecución predicada. Por lo tanto, tomé el primer código de philipp y mi código y lo ejecuté a través del compilador de ARM y el compilador de GCC para la arquitectura ARM, que presenta una ejecución predicada. Ambos compiladores evitan la rama en ambas muestras de código:
Versión de Philipp con el compilador ARM:
Versión de Philipp con GCC:
Mi código con el compilador ARM:
Mi código con GCC:
Todas las versiones aún necesitan una rama a la rutina de división, porque esta versión de ARM no tiene hardware para una división, pero la prueba
y == 0
se implementa completamente a través de la ejecución predicada.fuente
constexpr
y evitar tipos innecesarios como este:template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }
Y si quieres255
,(lhs)/(rhs+!rhs) & -!rhs
|
no&
. Ooops:( (lhs)/(rhs+!rhs) ) | -!rhs
debe establecer su valor en0xFFFFFFF
ifrhs
is0
ylhs/rhs
ifrhs!=0
.Aquí hay algunos números concretos, en Windows usando GCC 4.7.2:
Tenga en cuenta que no estoy llamando intencionalmente
srand()
, por lo querand()
siempre devuelve exactamente los mismos resultados. Tenga en cuenta también que-DCHECK=0
simplemente cuenta los ceros, por lo que es obvio con qué frecuencia apareció.Ahora, compilándolo y cronometrando de varias formas:
muestra la salida que se puede resumir en una tabla:
Si los ceros son raros, la
-DCHECK=2
versión funciona mal. A medida que comienzan a aparecer más ceros, el-DCHECK=2
caso comienza a funcionar significativamente mejor. De las otras opciones, realmente no hay mucha diferencia.Porque
-O3
, sin embargo, es una historia diferente:Allí, el cheque 2 no tiene inconvenientes en comparación con los otros cheques, y mantiene los beneficios a medida que los ceros se vuelven más comunes.
Sin embargo, realmente debería medir para ver qué sucede con su compilador y sus datos de muestra representativos.
fuente
d=0
aleatorio, en lugar de hacerlo casi siempred!=0
, y verá más fallas de predicción de rama. La predicción de ramas es excelente si una rama se sigue casi siempre, o si el seguimiento de una rama u otra es realmente grumoso ...d
iteración es el ciclo interno, por lo que losd == 0
casos se distribuyen uniformemente. ¿Esd == 0
realista el 50% de los casos ?0.002%
los casosd==0
? Se distribuyen por todas partes, cada 65000 iteraciones en sud==0
caso. Si bien50%
puede que no suceda con frecuencia,10%
o1%
podría suceder fácilmente, o incluso90%
o99%
. La prueba como se muestra solo prueba realmente "si básicamente nunca, nunca bajas por una rama, ¿la predicción de rama hace que eliminar la rama sea inútil?", A lo que la respuesta es "sí, pero eso no es interesante".Sin conocer la plataforma, no hay forma de conocer el método más eficiente exacto, sin embargo, en un sistema genérico esto puede acercarse al óptimo (usando la sintaxis de ensamblador Intel):
(suponga que el divisor está adentro
ecx
y el dividendo está adentroeax
)Cuatro instrucciones de ciclo único no ramificadas más la división. El cociente estará adentro
eax
y el restoedx
al final. (Este tipo de muestra por qué no desea enviar un compilador para hacer el trabajo de un hombre).fuente
De acuerdo con este enlace , puede bloquear la señal SIGFPE con
sigaction()
(no lo he probado yo mismo, pero creo que debería funcionar).Este es el enfoque más rápido posible si los errores de división por cero son extremadamente raros: solo paga por las divisiones por cero, no por las divisiones válidas, la ruta de ejecución normal no cambia en absoluto.
Sin embargo, el sistema operativo estará involucrado en todas las excepciones que se ignoren, lo cual es costoso. Creo que deberías tener al menos mil buenas divisiones por división entre cero que ignoras. Si las excepciones son más frecuentes que eso, probablemente pagará más ignorando las excepciones que verificando cada valor antes de la división.
fuente