fld - fadd assembly
Modelo de costo moderno x86 (6)
Estoy escribiendo un compilador JIT con un backend x86 y aprendiendo ensamblador x86 y código de máquina sobre la marcha.
El problema esencial aquí es que un compilador JIT no puede darse el lujo de gastar una gran cantidad de tiempo de micro-optimización. Debido a que la "optimización" ocurre en tiempo de ejecución, el costo de hacer optimizaciones debe ser menor que el tiempo ahorrado por las optimizaciones (de lo contrario, la optimización se convierte en una pérdida neta de rendimiento).
Para 80x86 hay múltiples CPU diferentes con diferentes comportamientos / características. Si se tienen en cuenta las características específicas de la CPU real, entonces el costo de hacer la optimización aumenta y se cierra directamente en una barrera de "cuesta más de lo que gana". Esto es especialmente cierto para cosas como "programación ideal de instrucciones".
Afortunadamente, la mayoría (pero no todas) de las CPU 80x86 modernas tienen varias características (fuera de orden, ejecución especulativa, hiper-threading) para mitigar (algunos de) los costos de rendimiento causados por la optimización "menos que perfecta". Esto tiende a hacer que las costosas optimizaciones sean menos beneficiosas.
Lo primero que debe hacer es identificar qué partes de código deben optimizarse y cuáles no. Las cosas que no se ejecutan con frecuencia (por ejemplo, el código de inicialización "solo se ejecutó una vez") no deben optimizarse en absoluto. Solo se trata de piezas ejecutadas con frecuencia (por ejemplo, bucles interiores, etc.) donde vale la pena molestarse. Una vez que haya identificado una pieza que vale la pena optimizar, la pregunta se convierte en "¿cuánto?".
Como una sobregeneralización cruda; Me gustaría esperar que (en promedio) el 90% del código no valga la pena optimizarlo, y para el 9% del código solo vale la pena hacer una optimización genérica. El 1% restante (que podría beneficiarse de una optimización exhaustiva en teoría) terminará siendo una molestia para el desarrollador del compilador JIT en la práctica (y resultaría en una pesadilla de complejidad / verificabilidad masiva, por ejemplo, "errores que solo existen cuando ejecutándose en algunos "escenarios" de CPUs.
Estoy escribiendo un compilador JIT con un backend x86 y aprendiendo ensamblador x86 y código de máquina sobre la marcha. Utilicé el ensamblador ARM hace unos 20 años y estoy sorprendido por la diferencia en los modelos de costos entre estas arquitecturas.
Específicamente, los accesos de memoria y las ramificaciones son costosos en ARM, pero las operaciones y saltos equivalentes de la pila son económicos en x86. Creo que las CPU x86 modernas optimizan mucho más que los núcleos ARM y me resulta difícil anticipar sus efectos.
¿Qué es un buen modelo de costo a tener en cuenta al escribir el ensamblador x86? ¿Qué combinaciones de instrucciones son baratas y cuáles son costosas?
Por ejemplo, mi compilador sería más simple si siempre generara la forma larga para cargar enteros o saltar a compensaciones, incluso si los enteros fueran pequeños o los desplazamientos se cierrasen, pero ¿afectaría esto el rendimiento?
Todavía no he hecho ningún punto flotante, pero me gustaría abordarlo pronto. ¿Hay algo que no sea obvio en la interacción entre el código normal y el código flotante?
Sé que hay muchas referencias (por ejemplo, Michael Abrash) sobre la optimización x86, pero tengo la corazonada de que nada más que hace unos años no se aplicará a las modernas CPU x86 porque han cambiado mucho últimamente. ¿Estoy en lo correcto?
La mejor referencia es el Manual de optimización de Intel , que proporciona información bastante detallada sobre riesgos arquitectónicos y latencias de instrucción para todos los núcleos recientes de Intel, así como una buena cantidad de ejemplos de optimización.
Otra excelente referencia son los recursos de optimización de Agner Fog , que tienen la virtud de cubrir también los núcleos de AMD.
Tenga en cuenta que los modelos de costos específicos son, por naturaleza, específicos de micro-arquitectura. No existe un "modelo de costo x86" que tenga ningún tipo de validez real. En el nivel de instrucción, las características de rendimiento de Atom son muy diferentes de i7.
También me gustaría señalar que los accesos a la memoria y las sucursales no son realmente "baratos" en los núcleos x86, es solo que el modelo de ejecución fuera de servicio se ha vuelto tan sofisticado que puede ocultar el costo de ellos en muchos escenarios simples.
Las latencias de instrucciones de Torbjörn Granlund y el rendimiento para los procesadores AMD e Intel x86 también es bueno.
Editar
El documento de Granlund se refiere al rendimiento de la instrucción en el contexto de cuántas instrucciones de un cierto tipo se pueden emitir por ciclo de reloj (es decir, se realiza en paralelo). También afirma que la documentación de Intel no siempre es precisa.
Merece la pena echar un vistazo a los compiladores existentes de código abierto de back-end, como GCC y LLVM. Estos tienen modelos para los costos de instrucción y también modelos de máquina decentes (pero idealizados) (por ejemplo, ancho del problema, tamaños de caché, etc.).
Por lo que vale, solía haber un libro increíble llamado "Inner Loops" por Rick Booth que describía en gran detalle cómo micro-optimizar manualmente el código ensamblador IA-86 para los procesadores Intel 80486, Pentium, Pentium Pro y Pentium MMX, con muchos ejemplos útiles del código del mundo real (hash, memoria móvil, generación de números aleatorios, compresión Huffman y JPEG, multiplicación de matrices).
Desafortunadamente, el libro no se ha actualizado desde su primera publicación en 1997 para procesadores más nuevos y arquitecturas de CPU. Sin embargo, aún así lo recomendaría como una introducción suave a temas tales como:
- qué instrucciones son generalmente muy baratas, o baratas, y cuáles no
- qué registros son los más versátiles (es decir, no tienen un significado especial / no son el registro predeterminado de algunas instrucciones)
- cómo emparejar las instrucciones para que se ejecuten en paralelo sin atascar una tubería
- diferentes tipos de puestos
- predicción de rama
- qué tener en cuenta con respecto a las memorias caché del procesador
Por supuesto, los informes de Agner Fog y el Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 son referencias necesarias y excelentes. AMD también tiene un manual de optimización:
- Guía de optimización de software para procesadores AMD Familia 15h
Sin embargo, dos herramientas de Intel son esenciales para comprender las secuencias de código:
- Analizador de código de arquitectura Intel®
- Intel® VTune ™
IACA es su modelo de costos. Lo uso en OSX, pero VTune solo se ejecuta en Windows y Linux.
También puede profundizar en la literatura de patentes de Intel y diversos documentos de Intel para comprender mejor cómo funcionan las cosas:
- La microarquitectura Intel Core de próxima generación
- Haswell: el procesador Intel Core de cuarta generación
- Caché de microoperaciones: un frontend de máxima potencia para la longitud de instrucción variable ISA