vulnerabilidad tiene spectre orden meltdown funciona fuera ejecución como bug c++ x86 branch-prediction speculative-execution

c++ - tiene - ¿Seguirá la ejecución especulativa a una operación costosa?



tiene ejecución fuera de orden (1)

Si entiendo la bifurcación correctamente (x86), el procesador tomará especulativamente una ruta de código y realizará las instrucciones y ''cancelará'' los resultados de la ruta incorrecta. ¿Qué pasa si la operación en la ruta de código incorrecta es muy costosa, como una lectura de memoria que causa una falta de caché o una operación matemática costosa? ¿Intentará el procesador realizar algo caro antes de tiempo? ¿Cómo podría un procesador manejar esto?

if (likely) { // do something lightweight (addition, subtraction, etc.) } else { // do something expensive (cache-miss, division, sin/cos/tan etc.) }


tl: dr : el impacto no es tan malo como piensas, porque la CPU ya no tiene que esperar por cosas lentas, incluso si no las cancela. Casi todo se canaliza en gran medida, por lo que muchas operaciones pueden estar en vuelo a la vez. Las operaciones mal especuladas no impiden que las nuevas comiencen.

Los diseños actuales de x86 no especulan en ambos lados de una rama a la vez. Solo especulan por el camino predicho.

No conozco ninguna microarquitectura específica que especule en ambos sentidos de una rama en ninguna circunstancia, pero eso no significa que no haya ninguna. En su mayoría, solo he leído sobre microarquitecturas x86 (consulte la wiki de etiquetas para ver los enlaces a la guía microarchivo de Agner Fog). Estoy seguro de que ha sido sugerido en documentos académicos, y tal vez incluso implementado en un diseño real en alguna parte.

No estoy seguro de qué sucede exactamente en los diseños actuales de Intel y AMD cuando se detecta una escritura incorrecta de una rama mientras una carga o almacenamiento de caché ya se está ejecutando pendiente, o una división está ocupando la unidad de división. Ciertamente, la ejecución fuera de servicio no tiene que esperar el resultado, porque ningún uops futuro depende de él.

En uarches distintos de P4, los uops falsos en el planificador / ROB se descartan cuando se detecta un error de predicción. Del documento microarchivo de Agner Fog, hablando de P4 frente a otros uarques:

la penalización por mal predicción es inusualmente alta por dos razones ... [larga duración y] ... las μops falsas en una rama mal predicha no se descartan antes de que se retiren. Una predicción errónea generalmente implica 45 μops. Si estos μops son divisiones u otras operaciones que requieren mucho tiempo, entonces la predicción errónea puede ser extremadamente costosa. Otros microprocesadores pueden descartar μops tan pronto como se detecte la predicción errónea para que no usen recursos de ejecución innecesariamente.

Los uops que actualmente ocupan unidades de ejecución son otra historia:

Casi todas las unidades de ejecución, excepto el divisor, están totalmente segmentadas, por lo que otra multiplicación, mezcla aleatoria o lo que sea puede comenzar sin cancelar una FP FMA en vuelo. (Haswell: latencia de 5 ciclos, dos unidades de ejecución con capacidad de procesamiento de uno por reloj, para un rendimiento total sostenido de uno por 0.5c. Esto significa que el rendimiento máximo requiere mantener 10 FMA en vuelo a la vez, normalmente con 10 acumuladores vectoriales). La división es interesante, sin embargo. La división de enteros es muchos uops, por lo que un error de predicción de ramas por lo menos dejará de emitirlas. FP div es solo una instrucción uop única, pero no está totalmente canalizada, especialmente. en CPUs anteriores. Sería útil cancelar un FP div que estaba ligando la unidad de división, pero IDK si eso es posible. Si agregar la capacidad de cancelar se hubiera ralentizado en el caso normal o le hubiera costado más potencia, entonces probablemente se omitirá. Es un raro caso especial en el que probablemente no valía la pena gastar transistores.

x87 fsin o algo es un buen ejemplo de una instrucción realmente costosa. No me di cuenta hasta que volví a volver a leer la pregunta. Está microcodificado, así que a pesar de que tiene una latencia de 47-106 ciclos (Intel Haswell), también es 71-100 uops. Una derivación incorrecta de la rama evitaría que la interfaz emitiera los uops restantes y cancelara todos los que están en cola, como dije para la división de enteros. Tenga en cuenta que las implementaciones de libm reales normalmente no usan fsin etc. porque son más lentas y menos precisas de lo que se puede lograr en el software (incluso sin SSE), IIRC.

Para una falta de caché, podría cancelarse, lo que podría ahorrar ancho de banda en la memoria caché L3 (y tal vez en la memoria principal). Incluso si no, la instrucción ya no tiene que retirarse, por lo que el ROB no se llenará esperando que termine. Por eso es por lo que las fallas en el caché perjudican mucho la ejecución de OOO, pero en este caso, en el peor de los casos, es atar una carga o almacenar el buffer. Las CPU modernas pueden tener muchas fallas de caché pendientes en vuelo a la vez. A menudo, el código no lo hace posible porque las operaciones futuras dependen del resultado de una carga que se perdió en la memoria caché (por ejemplo, la búsqueda de punteros en una lista o árbol vinculado), por lo que no se pueden canalizar operaciones de memoria múltiples. Incluso si un error de predicción de una rama no cancela gran parte de una operación de memoria en vuelo, evita la mayoría de los peores efectos.

He escuchado de poner un ud2 (instrucción ilegal) al final de un bloque de código para evitar que la ud2 instrucción desencadene una falla TLB cuando el bloque está al final de una página. No estoy seguro de cuándo esta técnica es necesaria. ¿Tal vez si hay una rama condicional que siempre se toma realmente? Eso no tiene sentido, solo usarías una rama incondicional. Debe haber algo de lo que no estoy recordando cuando harías eso.