Específicamente, si tengo una serie de if
... else if
declaraciones, y de alguna manera sé de antemano la probabilidad relativa de que cada declaración se evalúe true
, ¿cuánta diferencia en el tiempo de ejecución representa ordenarlas en orden de probabilidad? Por ejemplo, debería preferir esto:
if (highly_likely)
//do something
else if (somewhat_likely)
//do something
else if (unlikely)
//do something
¿a esto?:
if (unlikely)
//do something
else if (somewhat_likely)
//do something
else if (highly_likely)
//do something
Parece obvio que la versión ordenada sería más rápida, sin embargo, para facilitar la lectura o la existencia de efectos secundarios, es posible que desee ordenarlos de manera no óptima. También es difícil saber qué tan bien funcionará la CPU con la predicción de bifurcación hasta que realmente ejecute el código.
Entonces, en el transcurso de experimentar con esto, terminé respondiendo mi propia pregunta para un caso específico, sin embargo, también me gustaría escuchar otras opiniones / ideas.
Importante: esta pregunta supone que las if
declaraciones se pueden reordenar arbitrariamente sin tener ningún otro efecto sobre el comportamiento del programa. En mi respuesta, las tres pruebas condicionales son mutuamente excluyentes y no producen efectos secundarios. Ciertamente, si las declaraciones deben evaluarse en un cierto orden para lograr el comportamiento deseado, entonces el tema de la eficiencia es discutible.
Respuestas:
Como regla general, la mayoría de las CPU de Intel, si no todas, suponen que las ramas hacia adelante no se toman la primera vez que las ven. Ver el trabajo de Godbolt .
Después de eso, la rama entra en un caché de predicción de rama, y el comportamiento pasado se utiliza para informar la predicción de rama futura.
Entonces, en un circuito cerrado, el efecto de la falta de orden será relativamente pequeño. El predictor de rama aprenderá qué conjunto de ramas es más probable, y si tiene una cantidad de trabajo no trivial en el ciclo, las pequeñas diferencias no sumarán mucho.
En general, la mayoría de los compiladores por defecto (sin otro motivo) ordenarán el código de máquina producido aproximadamente de la manera en que lo ordenó en su código. Por lo tanto, si las declaraciones son ramas hacia adelante cuando fallan.
Por lo tanto, debe ordenar sus ramas en el orden de probabilidad decreciente de obtener la mejor predicción de rama de un "primer encuentro".
Un microbenchmark que se repite muchas veces en un conjunto de condiciones y realiza un trabajo trivial estará dominado por pequeños efectos del recuento de instrucciones y similares, y poco en cuanto a los problemas relativos de predicción de ramas. Entonces, en este caso, debe crear un perfil , ya que las reglas generales no serán confiables.
Además de eso, la vectorización y muchas otras optimizaciones se aplican a pequeños bucles estrechos.
Entonces, en el código general, coloque el código más probable dentro del
if
bloque, y eso dará como resultado la menor cantidad de errores de predicción de ramificación no almacenados en caché. En bucles ajustados, siga la regla general para comenzar, y si necesita saber más, no tiene más remedio que hacer un perfil.Naturalmente, todo esto desaparece si algunas pruebas son mucho más baratas que otras.
fuente
Hice la siguiente prueba para cronometrar la ejecución de dos
if
...else if
bloques diferentes , uno ordenado en orden de probabilidad y el otro en orden inverso:Usando MSVC2017 con / O2, los resultados muestran que la versión ordenada es consistentemente un 28% más rápida que la versión no ordenada. Según el comentario de luk32, también cambié el orden de las dos pruebas, lo que hace una diferencia notable (22% frente a 28%). El código se ejecutó bajo Windows 7 en un Intel Xeon E5-2697 v2. Esto es, por supuesto, muy específico del problema y no debe interpretarse como una respuesta concluyente.
fuente
if... else if
declaración podría tener un efecto sustancial sobre cómo fluye la lógica a través del código. Esunlikely
posible que el cheque no salga a menudo, pero puede ser necesario que la empresa verifiqueunlikely
primero la condición antes de buscar otros.g++ -O2 -march=native -std=c++14
da una ligera ventaja a las declaraciones condicionales ordenadas, pero la mayoría de las veces, la diferencia porcentual entre las dos ejecuciones fue de ~ 5%. Varias veces, en realidad fue más lento (debido a las variaciones). Estoy bastante seguro de queif
no vale la pena preocuparse por ordenar este tipo de mensajes; PGO probablemente se encargará por completo de tales casosNo, no debería, a menos que esté realmente seguro de que el sistema de destino se ve afectado. Por defecto ir por legibilidad.
Dudo mucho sus resultados. He modificado un poco su ejemplo, por lo que invertir la ejecución es más fácil. Ideone muestra consistentemente que el orden inverso es más rápido, aunque no mucho. En ciertas carreras, incluso esto ocasionalmente se volteó. Yo diría que los resultados no son concluyentes. Coliru tampoco informa una diferencia real. Puedo verificar la CPU Exynos5422 en mi odroid xu4 más adelante.
La cuestión es que las CPU modernas tienen predictores de rama. Hay mucha, mucha lógica dedicada a buscar datos e instrucciones, y las CPU modernas x86 son bastante inteligentes, cuando se trata de esto. Algunas arquitecturas más delgadas como ARM o GPU pueden ser vulnerables a esto. Pero depende mucho del compilador y del sistema de destino.
Yo diría que la optimización de pedidos de sucursales es bastante frágil y efímera. Hazlo solo como un paso de ajuste realmente fino.
Código:
fuente
Solo mis 5 centavos. Parece el efecto de ordenar si las declaraciones deberían depender de:
Probabilidad de cada enunciado if.
Número de iteraciones, por lo que el predictor de rama podría entrar en acción.
Sugerencias de compilación probables / improbables, es decir, diseño de código.
Para explorar esos factores, comparé las siguientes funciones:
shown_ifs ()
reversed_ifs ()
shown_ifs_with_hints ()
reversed_ifs_with_hints ()
datos
La matriz de datos contiene números aleatorios entre 0 y 100:
Los resultados
Los siguientes resultados son para Intel i5 @ 3,2 GHz y G ++ 6.3.0. El primer argumento es check_point (es decir, probabilidad en %% para la declaración if altamente probable), el segundo argumento es data_sz (es decir, número de iteraciones).
Análisis
1. El pedido sí importa
Para iteraciones 4K y (casi) 100% de probabilidad de afirmaciones muy apreciadas, la diferencia es enorme 223%:
Para las iteraciones 4K y el 50% de probabilidad de afirmaciones muy apreciadas, la diferencia es de aproximadamente el 14%:
2. El número de iteraciones importa
La diferencia entre las iteraciones 4K y 8K para (casi) el 100% de probabilidad de afirmaciones muy apreciadas es aproximadamente dos veces (como se esperaba):
Pero la diferencia entre las iteraciones 4K y 8K para una probabilidad del 50% de afirmaciones muy apreciadas es 5,5 veces:
¿Por qué es así? Debido a la falta de predictores de rama. Aquí está la rama se pierde para cada caso mencionado anteriormente:
Entonces, en mi i5, el predictor de bifurcación falla espectacularmente para bifurcaciones poco probables y grandes conjuntos de datos.
3. Consejos ayudan un poco
Para las iteraciones 4K, los resultados son algo peores para una probabilidad del 50% y algo mejores para una probabilidad cercana al 100%:
Pero para las iteraciones de 8K, los resultados siempre son un poco mejores:
Por lo tanto, las sugerencias también ayudan, pero solo un poquito.
La conclusión general es: siempre compara el código, porque los resultados pueden sorprender.
Espero que ayude.
fuente
g++ -O2
o-O3 -fno-tree-vectorize
, pero deberías decirlo.Basado en algunas de las otras respuestas aquí, parece que la única respuesta real es: depende . Depende al menos de lo siguiente (aunque no necesariamente en este orden de importancia):
La única forma de saberlo con certeza es comparar su caso específico, preferiblemente en un sistema idéntico (o muy similar) al sistema previsto en el que finalmente se ejecutará el código. Si está destinado a ejecutarse en un conjunto de sistemas variables con hardware, sistema operativo, etc. diferente, entonces es una buena idea comparar con múltiples variaciones para ver cuál es el mejor. Incluso puede ser una buena idea que el código se compile con un pedido en un tipo de sistema y otro pedido en otro tipo de sistema.
Mi regla general personal (para la mayoría de los casos, en ausencia de un punto de referencia) es ordenar según:
fuente
La forma en que generalmente veo esto resuelto para el código de alto rendimiento es mantener el orden más legible, pero proporcionar pistas al compilador. Aquí hay un ejemplo del kernel de Linux :
Aquí se supone que pasará la verificación de acceso y que no se devolverá ningún error
res
. Intentar reordenar cualquiera de estas cláusulas si solo confundiría el código, pero las macroslikely()
yunlikely()
realmente ayudan a la legibilidad al señalar cuál es el caso normal y cuál es la excepción.La implementación de Linux de esas macros utiliza características específicas de GCC . Parece que clang y el compilador Intel C admiten la misma sintaxis, pero MSVC no tiene esa característica .
fuente
likely()
yunlikely()
se definen las macros, e incluir alguna información acerca de la característica del compilador correspondiente.else if
si el compilador no es lo suficientemente inteligente como para saber que las condiciones son mutuamente excluyentes.También depende de su compilador y la plataforma para la que está compilando.
En teoría, la condición más probable debería hacer que el control salte lo menos posible.
Por lo general, la condición más probable debe ser primero:
Los asm más populares se basan en ramas condicionales que saltan cuando la condición es verdadera . Ese código C probablemente se traducirá a tal pseudoasm:
Esto se debe a que los saltos hacen que la CPU cancele la canalización de ejecución y se bloquee porque el contador del programa cambió (para arquitecturas que admiten tuberías que son realmente comunes). Luego se trata del compilador, que puede o no aplicar algunas optimizaciones sofisticadas sobre tener la condición estadísticamente más probable para que el control haga menos saltos.
fuente
clang
en realidad tomó un enfoque diferente paratest2
ytest3
: a causa de la heurística que indican que una< 0
o== 0
prueba es probable que sea falsa, decidió clonar el resto de la función en ambos caminos, por lo que es capaz de hacer que lacondition == false
de la caída a través del camino. Esto es factible solo porque el resto de la función es breve:test4
agregué una operación más y volví al enfoque que describí anteriormente.jmp
no son útil para desperdiciar / descodificar el ancho de banda (2) incluso con la predicción, los núcleos grandes modernos solo hacen una búsqueda por ciclo, por lo que pone un límite estricto de 1 rama / ciclo tomado (OTOH Intel moderno puede hacer 2 no tomados / ciclo) (3 ) es más difícil para la predicción de rama tratar con ramas consecutivas tomadas y en el caso de predictores rápidos + lentos ...Decidí volver a ejecutar la prueba en mi propia máquina usando el código Lik32. Tuve que cambiarlo debido a que mi compilador o Windows pensaba que la alta resolución es de 1 ms, usando
mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -fexceptions -g
GCC ha realizado la misma transformación en ambos códigos originales.
Tenga en cuenta que solo las dos primeras condiciones se prueban, ya que la tercera siempre debe ser cierta, GCC es una especie de Sherlock aquí.
Contrarrestar
Entonces, esto no nos dice mucho, excepto que el último caso no necesita una predicción de rama.
Ahora probé las 6 combinaciones de los if, los 2 primeros son el inverso original y ordenados. alto es> = 95, bajo es <20, medio es 20-94 con 10000000 iteraciones cada uno.
Entonces, ¿por qué el orden es alto, bajo, medio y luego más rápido (marginalmente)
Porque lo más impredecible es el último y, por lo tanto, nunca se ejecuta a través de un predictor de rama.
Entonces las ramas serán predichas tomadas, tomadas y el resto con
6% + (0.94 *) 20% de predicciones erróneas.
"Ordenado"
Las ramas serán predichas con no tomado, no tomado y Sherlock.
25% + (0.75 *) 24% predicciones erróneas
Dando una diferencia del 18-23% (diferencia medida de ~ 9%) pero necesitamos calcular ciclos en lugar de predecir erróneamente el%.
Supongamos una penalización de predicción errónea de 17 ciclos en mi CPU Nehalem y que cada verificación demora 1 ciclo en emitirse (4-5 instrucciones) y el ciclo también toma un ciclo. Las dependencias de datos son los contadores y las variables de bucle, pero una vez que las predicciones erróneas están fuera del camino, no debería influir en el tiempo.
Entonces, para "reversa", obtenemos los tiempos (esta debería ser la fórmula utilizada en Computer Architecture: A Quantitative Approach IIRC).
y lo mismo para "ordenado"
(8.26-7.24) /8.26 = 13.8% vs. ~ 9% medido (¡cerca del medido!?!).
Entonces, lo obvio del OP no es obvio.
Con estas pruebas, otras pruebas con código más complicado o más dependencias de datos serán diferentes, así que mida su caso.
Cambiar el orden de la prueba cambió los resultados, pero eso podría deberse a diferentes alineamientos del inicio del bucle, que idealmente deberían estar alineados a 16 bytes en todas las CPU Intel más nuevas, pero no en este caso.
fuente
Póngalos en el orden lógico que desee. Claro, la ramificación puede ser más lenta, pero la ramificación no debería ser la mayoría del trabajo que está haciendo su computadora.
Si está trabajando en una porción de código de rendimiento crítico, entonces ciertamente use el orden lógico, la optimización guiada por el perfil y otras técnicas, pero para el código general, creo que es más una elección estilística.
fuente
i++
cuándo++i
lo haría, porque soy consciente de quei++
para algunos iteradores es difícil optimizarlo++i
y la diferencia (para mí) no importa. Se trata de evitar la pesimismo; poner el bloque más probable primero como un hábito predeterminado no causará una reducción notable de legibilidad (¡y en realidad podría ayudar!), al tiempo que da como resultado un código que es amigable para la predicción de ramas (y por lo tanto le brinda un pequeño aumento uniforme de rendimiento que no se puede recuperar) por micro optimización posterior)Si ya conoce la probabilidad relativa de la declaración if-else, entonces, para fines de rendimiento, sería mejor usar la forma ordenada, ya que solo verificará una condición (la verdadera).
De manera no ordenada, el compilador verificará todas las condiciones innecesariamente y llevará tiempo.
fuente