Discrepancia de tiempo de BR/RET al regresar de una subrutina ficticia a una dirección de retorno modificada.
En mis aventuras experimentando con la arquitectura ARM de 64 bits, noté una peculiar diferencia de velocidad dependiendo de si se usa “br” o “ret” para regresar de una subrutina.
; Fabricado con fines de aprendizaje/experimentación únicamente, sin ningún uso práctico
foo:
cmp w0, #0
b.eq .L0
sub w0, w0, #1
sub x30, x30, #4
ret
.L0:
ret ; intencionalmente duplicado 'ret'
La intención de esta subrutina es hacer que el llamador de “foo” “reingrese” a “foo” “w0” veces haciendo que “foo” regrese a la instrucción que llamó a “foo” en primer lugar (es decir, la instrucción inmediatamente antes de aquella a la que apunta “x30”). Con una temporización aproximada, con “w0” siendo un valor lo suficientemente alto, tomó alrededor de 1362 milisegundos en promedio. Curiosamente, al reemplazar el primer “ret” con “br x30”, se ejecuta más de dos veces más rápido, tomando solo unos 550 milisegundos en promedio.
La discrepancia de tiempo desaparece si la prueba se simplifica a solo llamar repetidamente a una subrutina con solo “ret” / “br x30”. ¿Qué hace que la subrutina artificial anterior sea más lenta con un “ret”?
Probé esto en algún tipo de procesador ARMv8.2 (Cortex-A76 + Cortex-A55). No estoy seguro de hasta qué punto big.LITTLE afectaría los tiempos, pero parecían bastante consistentes en múltiples ejecuciones. Esto de ninguna manera es una prueba de referencia [micro], sino más bien algo como “aproximadamente cuánto tiempo tarda esto si se ejecuta N veces”.
davy.ai
La diferencia de velocidad entre utilizar
br x30
yret
en la subrutina creada se puede atribuir al comportamiento de la predicción de saltos en los procesadores modernos.br x30
es una instrucción de salto directo, lo que significa que siempre va a la dirección almacenada enx30
, y el procesador puede predecir fácilmente desde dónde se buscará la siguiente instrucción. En cambio,ret
es una instrucción de salto indirecto que vuelve a la dirección almacenada en el registro de enlacex30
. Dado que el valor exacto dex30
dependerá de la historia de ejecución del programa, la predicción de saltos se vuelve mucho más difícil pararet
.Los procesadores modernos utilizan una variedad de técnicas para predecir el destino de los saltos indirectos, como mantener un historial de destinos de saltos anteriores y utilizar esa información para predecir futuros destinos. Sin embargo, esta predicción no es perfecta, y cuando la predicción es incorrecta, hay una penalización en el rendimiento ya que el procesador tiene que vaciar su tubería y volver a buscar las instrucciones desde el destino correcto.
En la subrutina creada, el uso repetido de
ret
significa que el procesador tiene que predecir repetidamente el destino de un salto indirecto, lo que lleva a una penalización en el rendimiento. Cuando se utiliza en cambiobr x30
, no es necesario utilizar la predicción de saltos y se evita la penalización en el rendimiento.Vale la pena señalar que la disminución de velocidad observada en la subrutina creada puede no ser notable en código del mundo real, especialmente si hay otros saltos o instrucciones que permiten al procesador recuperarse de predicciones incorrectas. En general, es mejor utilizar la instrucción de salto adecuada para la situación y no depender de microbenchmarks para tomar decisiones de rendimiento.