tutorial smart remix español curso aprender c++ optimization error-handling cpu-architecture branch-prediction

c++ - smart - Predicción de rama y división por cero



solidity español (4)

Estaba escribiendo un código que se parecía a lo siguiente ...

if(denominator == 0){ return false; } int result = value / denominator;

... cuando pensé en el comportamiento de bifurcación en la CPU.

https://stackoverflow.com/a/11227902/620863 Esta respuesta dice que la CPU intentará adivinar correctamente hacia dónde irá una rama, y ​​dirigirse hacia esa rama solo para detenerse si descubre que adivinó la rama incorrectamente.

Pero si la CPU predice incorrectamente la bifurcación de arriba, se dividirá por cero en las siguientes instrucciones. Sin embargo, esto no sucede, y me preguntaba por qué. ¿La CPU realmente ejecuta una división por cero y espera para ver si la rama es correcta antes de hacer cualquier cosa, o puede decir que no debería continuar en estas situaciones? ¿Que esta pasando?


Pero si la CPU predice incorrectamente la bifurcación de arriba, se dividirá por cero en las siguientes instrucciones. Sin embargo, esto no sucede, y me preguntaba por qué.

Puede suceder, sin embargo, la pregunta es: ¿es observable? Obviamente, esta división especulativa por cero no debe ni debe "colapsar" la CPU, pero esto ni siquiera ocurre para una división no especulativa por cero. Hay una larga cadena causal entre la división por cero y su proceso sale con un mensaje de error. Es algo como esto (en POSIX, x86):

  • La ALU o el microcódigo responsable de la división señala la división por cero como un error.
  • El descriptor de interrupción # 0 está cargado (int 0 significa una división por error cero en x86).
  • Se inserta un conjunto de registros (incluido el contador del programa actual) en la pila. Es posible que las líneas de caché correspondientes deban buscarse primero desde la RAM.
  • El controlador de interrupción se ejecuta (una parte del código del kernel). Levanta una señal SIGFPE en el proceso actual.
  • Finalmente, el manejo de la señal decide que se tomará la acción predeterminada (suponiendo que no haya instalado un controlador), que es mostrar un mensaje de error y finalizar el proceso.
  • Esto requiere muchos pasos adicionales (por ejemplo, el uso de controladores de dispositivo) hasta que el usuario observe un cambio, es decir, algunos gráficos emitidos por E / S mapeadas en memoria.

Esto es mucho trabajo, en comparación con una división simple, libre de errores, y mucho de eso podría ejecutarse especulativamente. Básicamente cualquier cosa hasta la E / S mmap''ed real, o hasta que se agote el conjunto finito de recursos para la ejecución especulativa (por ejemplo, registros paralelos y líneas de caché temporal). Lo último es probable que suceda mucho, mucho antes. En este caso, la rama especulativa debe suspenderse, hasta que quede claro si se toma realmente y los cambios se deben comprometer (una vez que se escriben los cambios, se pueden liberar los recursos de ejecución especulativa), o si los cambios deberían ser descartado.

El aspecto importante es: siempre que ninguno de los estados especulativos de ejecución sea visible para otros hilos, otras ramas especulativas en el mismo hilo u otro hardware (como gráficos), todo vale para la optimización. Sin embargo, de manera realista, MSalters tiene toda la razón en que a un diseñador de CPU no le importaría optimizar para este caso de uso. Entonces, también es mi opinión, que una CPU real probablemente solo suspenda la rama especulativa una vez que se establezca el indicador de error. Esto a lo sumo cuesta unos pocos ciclos si el error es incluso legítimo , e incluso eso es poco probable porque el patrón que describió es común. Realizar una ejecución especulativa más allá de este punto solo desviaría valiosos recursos de optimización de casos más importantes.

(De hecho, la única excepción de procesador que quisiera hacer es razonablemente rápida, si fuera un diseñador de CPU, es un tipo específico de error de página, donde la página es conocida y accesible, pero el indicador "presente" se borra, solo porque esto ocurre comúnmente cuando se usa memoria virtual, y no es un verdadero error. Incluso este caso no es terriblemente importante, porque el acceso al disco al intercambiar, o incluso solo la descompresión de la memoria, suele ser mucho más costoso).


La CPU es libre de hacer lo que quiera, cuando ejecuta especulativamente una rama en base a una predicción. Pero tiene que hacerlo de una manera que sea transparente para el usuario. Por lo tanto, puede presentar una falla de "división por cero", pero esto debería ser invisible si la predicción de bifurcación resulta incorrecta. Por la misma lógica, puede crear escrituras en la memoria, pero puede que no las comprometa.

Como diseñador de CPU, no me molestaría predecir más allá de tal falla. Eso probablemente no valga la pena. El error probablemente signifique una mala predicción, y eso se resolverá muy pronto.

Esta libertad es algo bueno. Considere un simple std::accumulate loop. El predictor de bifurcación predecirá correctamente una gran cantidad de saltos ( for (auto current = begin, current != end; ++current) que por lo general salta de nuevo al principio del bucle), y hay muchas lecturas de memoria que pueden potencialmente fallar ( sum += *current ). Pero una CPU que se negaría a leer un valor de memoria hasta que se haya resuelto la rama anterior sería mucho más lenta. Y, sin embargo, un salto incorrecto al final del ciclo podría causar una falla de memoria inofensiva, ya que la rama predicha intenta leer más allá del búfer. Esto debe resolverse sin una falla visible.


La división por cero no es nada realmente especial. Es una condición que maneja la ALU para producir algún efecto, como asignar un valor especial al cociente. También puede generar una excepción si este tipo de excepción se ha habilitado.

Comparar con el fragmento

if (denominator == 0) { return false; } int result = value * denominator;

El multiplicador se puede ejecutar de forma especulativa, luego se cancela sin que lo sepas. Lo mismo para una división. Sin preocupaciones.


No exactamente. El sistema no puede ejecutar las instrucciones en la rama incorrecta, incluso si adivina mal, o más exactamente si lo hace no debe ser visible. El básico es:

  • hay una prueba en alguna parte del código de máquina.
  • el procesador carga la tubería con instrucciones en una de las posibles rutas y posiblemente las ejecuta internamente . De acuerdo con MSalters, algunos procesadores podrían incluso ejecutar ambas rutas (*)
  • si fue una buena suposición, está bien, las siguientes instrucciones han sido precargadas en el caché del procesador o ya ejecutadas, y todo va lo más rápido posible
  • si hizo una suposición incorrecta, solo tiene que limpiar todo y reiniciar en la rama correcta.

Para la analogía con la publicación referenciada, el tren tiene que detenerse inmediatamente en el cruce si el interruptor no estaba en la posición correcta, no puede ir a la siguiente estación en el camino equivocado, o si no puede detenerse antes de eso, no se permitirán pasajeros entrar o salir del tren

(*) Los procesadores Itanium podrían procesar muchas rutas en paralelo. La lógica de Intel era que podían construir procesadores anchos (que hacen mucho en paralelo) pero estaban luchando con la tasa de instrucción efectiva. Ejecutando especulativamente ambas ramas, utilizaron una gran cantidad de hardware (creo que podrían hacerlo a varios niveles de profundidad, ejecutando 2 ^ N ramas) pero sí ayudó a la aparente velocidad de núcleo único, ya que en efecto siempre predijo la rama correcta en un HW unidad: los créditos deben ir a MSalters para esa precisión