performance - unity - Efectos de la predicción de bifurcación en el rendimiento?
wpo calidad (7)
"(Por el bien de la discusión, supongamos que estoy más allá de la fase de" optimización temprana es la raíz de todo mal ")"
Excelente. Luego puede perfilar el rendimiento de su aplicación, usar las etiquetas de gcc para hacer una predicción y un perfil nuevamente, usar las etiquetas de gcc para hacer nuevamente la predicción y el perfil opuesto.
Ahora imagine teóricamente una CPU que prefija ambas rutas de bifurcación. Y para sentencias if posteriores en ambas rutas, precapturará cuatro rutas, etc. La CPU no crece mágicamente cuatro veces el espacio de caché, por lo que va a captar previamente una parte más corta de cada ruta de lo que lo haría con una ruta única.
Si encuentra que la mitad de las captaciones previas se desperdician, pierda decir el 5% de su tiempo de CPU, entonces desea buscar una solución que no se bifurque.
Cuando estoy escribiendo un ciclo cerrado que necesita trabajar rápido, a menudo me molestan las ideas sobre cómo se comportará la predicción de la rama del procesador. Por ejemplo, hago mi mejor esfuerzo para evitar tener una instrucción if en el ciclo más interno, especialmente una con un resultado que no es algo uniforme (por ejemplo, evalúa como verdadero o falso al azar).
Tiendo a hacer eso debido al conocimiento bastante común de que el procesador preselecciona las instrucciones y si resulta que predice mal una rama, entonces la precarga es inútil.
Mi pregunta es: ¿esto realmente es un problema con los procesadores modernos? ¿Qué tan bueno se espera que sea la predicción de rama?
¿Qué patrones de codificación pueden usarse para mejorarlo?
(Por el bien de la discusión, supongamos que estoy más allá de la fase de "optimización temprana es la raíz de todo mal")
Mi respuesta es:
La razón por la cual AMD ha sido tan rápido o mejor que Intel en algunos puntos es que el pasado es simplemente que tenían una mejor predicción de bifurcación.
Si su código no tiene predicción de bifurcación (significa que no tiene ramificaciones), se puede esperar que se ejecute más rápido.
Entonces, conclusión: evita las ramas si no son necesarias. Si lo son, intente hacerlo de modo que una rama se evalúe el 95% del tiempo.
No es exactamente una respuesta, pero puede encontrar aquí un applet que muestra la máquina de estados finitos que se usa a menudo para la predicción de ramificaciones basada en tablas en microprocesadores modernos .
Ilustra la lógica adicional de uso para generar una estimación rápida (pero posiblemente incorrecta) para la condición de la sucursal y la dirección del objetivo.
El procesador recupera y ejecuta las instrucciones pronosticadas a toda velocidad, pero necesita revertir todos los resultados intermedios cuando la predicción resultó ser incorrecta.
Si estamos más allá de la fase de "optimización temprana", entonces seguramente también estamos más allá de la fase "Puedo medir eso". Con las complejas complejidades de la arquitectura de CPU moderna, la única forma de saberlo con certeza es probarlo y medirlo. Seguramente no puede haber tantas circunstancias en las que tendrá la opción de dos maneras de implementar algo, una de las cuales requiere una rama y otra que no.
Una cosa que encontré recientemente (en un DSP de TI) es que tratar de evitar las ramas a veces puede generar más código que el costo de predicción de la sucursal.
Tuve algo así como lo siguiente en un círculo cerrado:
if (var >= limit) { otherVar = 0;}
Quería deshacerme de la posible rama e intenté cambiarla por:
otherVar *= (var<limit)&1;
Pero la ''optimización'' generó el doble de ensamblaje y en realidad fue más lenta.
La predicción de la sucursal es bastante buena en estos días. Pero eso no significa que se pueda eliminar la penalización de las sucursales.
En el código típico, es probable que obtenga más del 99% de predicciones correctas y, sin embargo, el rendimiento alcanzado todavía puede ser significativo. Hay varios factores en juego en esto.
Una es la latencia de ramificación simple. En una CPU de PC común, puede ser del orden de 12 ciclos para una escritura errónea, o de 1 ciclo para una rama predicha correctamente. En aras de la discusión, supongamos que todas sus ramas están pronosticadas correctamente, entonces estás en casa libre, ¿no? No exactamente.
La simple existencia de una rama inhibe muchas optimizaciones. El compilador no puede reordenar el código de manera eficiente en todas las sucursales. Dentro de un bloque básico (es decir, un bloque de código que se ejecuta secuencialmente, sin ramas, un punto de entrada y una salida), puede reordenar las instrucciones como quiera, siempre que se preserve el significado del código, porque Todo será ejecutado tarde o temprano. En todas las ramas, se vuelve más complicado. Podríamos mover estas instrucciones para ejecutarlas después de esta rama, pero ¿cómo garantizamos que se ejecuten? Ponerlos en ambas ramas? Eso es un tamaño de código adicional, eso también es complicado, y no escala si queremos reordenar en más de una rama.
Las sucursales aún pueden ser costosas, incluso con la mejor predicción de sucursal. No solo por errores de predicción, sino porque la programación de instrucciones se vuelve mucho más difícil.
Esto también implica que, más que el número de ramas, el factor importante es la cantidad de código que entra en el bloque entre ellas. Una rama en cada otra línea es mala, pero si puede obtener una docena de líneas en un bloque entre sucursales, probablemente sea posible obtener esas instrucciones programadas razonablemente bien, por lo que la rama no restringirá demasiado la CPU o el compilador.
Pero en el código típico, las ramas son esencialmente libres. En el código típico, no hay muchas ramas agrupadas juntas en el código de rendimiento crítico.
Sí, la predicción de ramas realmente puede ser un problema de rendimiento.
Esta pregunta (actualmente la pregunta más votado sobre ) da un ejemplo.