assembly cpu cycle

assembly - ¿Cuántos ciclos de CPU se necesitan para cada instrucción de ensamblaje?



cycle (5)

Las CPUs modernas son bestias complejas, que utilizan la pipelining , la ejecución superescalar y la ejecución desordenada , entre otras técnicas que dificultan el análisis del rendimiento ... ¡ pero no imposible !

Aunque ya no puede agregar las latencias de una secuencia de instrucciones para obtener el tiempo de ejecución total, aún puede obtener un análisis (a menudo) muy preciso del comportamiento de algún fragmento de código (especialmente un ciclo) como se describe a continuación y en otros recursos vinculados.

Tiempos de instrucción

Primero, necesitas los tiempos reales. Estos varían según la arquitectura de la CPU, pero el mejor recurso actualmente para los tiempos de x86 son las tablas de instrucciones de Agner Fog. Cubriendo no menos de treinta microarchitecures diferentes, estas tablas enumeran la latencia de la instrucción, que es el tiempo mínimo / típico que una instrucción toma desde las entradas listas para la salida disponible. En palabras de Agner:

Latencia: este es el retraso que genera la instrucción en una cadena de dependencia. Los números son valores mínimos. Las fallas de caché, la desalineación y las excepciones pueden aumentar considerablemente el conteo de los relojes. Donde hyperthreading está habilitado, el uso de las mismas unidades de ejecución en el otro hilo conduce a un rendimiento inferior. Los números denormales, NAN e infinito no aumentan la latencia. La unidad de tiempo utilizada es ciclos de reloj de núcleo, no los ciclos de reloj de referencia dados por el contador de marca de tiempo.

Entonces, por ejemplo, la instrucción add tiene una latencia de un ciclo, por lo que una serie de instrucciones de add dependientes , como se muestra, tendrá una latencia de 1 ciclo por add :

add eax, eax add eax, eax add eax, eax add eax, eax # total latency of 4 cycles for these 4 adds

Tenga en cuenta que esto no significa que add instrucciones solo tomará 1 ciclo cada una. Por ejemplo, si las instrucciones de agregar no son dependientes, es posible que en las fichas modernas, las 4 instrucciones de agregar se puedan ejecutar de forma independiente en el mismo ciclo:

add eax, eax add ebx, ebx add ecx, ecx add edx, edx # these 4 instructions might all execute, in parallel in a single cycle

Agner proporciona una métrica que captura parte de este paralelismo potencial, llamado rendimiento recíproco :

Rendimiento recíproco: el número promedio de ciclos de reloj central por instrucción para una serie de instrucciones independientes del mismo tipo en el mismo hilo.

Para add esto se lista como 0.25 lo que significa que se pueden ejecutar hasta 4 instrucciones de add cada ciclo (dando un rendimiento recíproco de 1/4 1 / 4 = 0.25 ).

El número de rendimiento recíproco también da una pista sobre la capacidad de canalización de una instrucción. Por ejemplo, en los chips x86 más recientes, las formas comunes de la instrucción imul tienen una latencia de 3 ciclos, e internamente solo una unidad de ejecución puede manejarlas (a diferencia de add que generalmente tiene cuatro unidades con capacidad de agregar). Sin embargo, el rendimiento observado para una larga serie de instrucciones de imul independientes es 1 / ciclo, no 1 cada 3 ciclos, como cabría esperar dada la latencia de 3. La razón es que la unidad imul está canalizada: puede iniciar un nuevo imul cada ciclo , incluso aunque la multiplicación anterior no se haya completado.

Esto significa que una serie de instrucciones de imul independientes pueden ejecutarse hasta 1 por ciclo, pero una serie de instrucciones de imul dependientes se ejecutará a solo 1 cada 3 ciclos (ya que el siguiente imul no puede comenzar hasta que el resultado del previo esté listo )

Entonces, con esta información, puede comenzar a ver cómo analizar los tiempos de instrucción en las CPU modernas.

Análisis detallado

Aún así, lo anterior solo araña la superficie. Ahora tiene varias maneras de ver una serie de instrucciones (latencia o rendimiento) y puede no estar claro cuál usar.

Además, hay otros límites no capturados por los números anteriores, como el hecho de que ciertas instrucciones compiten por los mismos recursos dentro de la CPU y las restricciones en otras partes de la tubería de la CPU (como la descodificación de instrucciones) que pueden resultar en una menor rendimiento total que usted calcule solo mirando la latencia y el rendimiento. Más allá de eso, tienes factores "más allá de las ALU", como el acceso a la memoria y la predicción de ramas: temas enteros en sí mismos: puedes modelarlos en su mayoría, pero se necesita trabajo. Por ejemplo, aquí hay una publicación reciente donde la respuesta cubre en detalle la mayoría de los factores relevantes.

Cubrir todos los detalles aumentaría el tamaño de esta respuesta ya larga en un factor de 10 o más, así que solo te indicaré los mejores recursos. Agner Fog tiene una guide Optimizing Asembly que cubre en detalle el análisis preciso de un bucle con aproximadamente una docena de instrucciones. Consulte " 12.7 Un ejemplo de análisis de cuellos de botella en bucles de vector" que comienza en la página 95 en la versión actual del PDF.

La idea básica es crear una tabla, con una fila por instrucción y marcar los recursos de ejecución que cada uno usa. Esto le permite ver los cuellos de botella de rendimiento. Además, debe examinar el ciclo para las dependencias transportadas, para ver si alguno de ellos limita el rendimiento (consulte " 12.16 Análisis de dependencias" para un caso complejo).

Si no quiere hacerlo a mano, Intel lanzó Intel Architecture Code Analyzer , que es una herramienta que automatiza este análisis. Actualmente no se ha actualizado más allá de Skylake, pero los resultados son en gran medida razonables para Kaby Lake ya que la microarquitectura no ha cambiado mucho y, por lo tanto, los tiempos siguen siendo comparables. Esta respuesta entra en muchos detalles y proporciona resultados de ejemplo, y la guía del usuario no es mala (aunque está desactualizada con respecto a las versiones más recientes).

Otras fuentes

Agner generalmente proporciona tiempos para las nuevas arquitecturas poco después de su lanzamiento, pero también puede consultar instlatx64 para conocer los tiempos organizados de manera similar en los resultados de InstLatX86 e InstLatX64 . Los resultados cubren una gran cantidad de chips antiguos interesantes, y los nuevos chips generalmente aparecen bastante rápido. Los resultados son en su mayoría consistentes con los de Agner, con algunas excepciones aquí y allá. También puede encontrar latencia de memoria y otros valores en esta página.

Incluso puede obtener los resultados de sincronización directamente de Intel en su manual de optimización IA32 e Intel 64 en el Apéndice C: LATENCIA DE INSTRUCCIÓN Y ENTRADA . Personalmente, prefiero la versión de Agner porque son más completas, suelen llegar antes de que se actualice el manual de Intel y son más fáciles de usar, ya que proporcionan una hoja de cálculo y una versión en PDF.

Finalmente, la wiki de etiquetas x86 tiene una gran cantidad de recursos en optimización x86, que incluyen enlaces a otros ejemplos de cómo hacer un ciclo de análisis preciso de secuencias de códigos.

Si desea una mirada más profunda sobre el tipo de "análisis de flujo de datos" descrito anteriormente, recomendaría una introducción de torbellino a los gráficos de flujo de datos .

Escuché que hay un libro de Intel en línea que describe los ciclos de CPU necesarios para una instrucción de ensamblaje específica, pero no puedo encontrarlo (después de intentarlo). ¿Alguien podría mostrarme cómo encontrar el ciclo de la CPU, por favor?

Aquí hay un ejemplo, en el siguiente código, mov / lock es 1 ciclo de CPU, y xchg es 3 ciclos de CPU.

// This part is Platform dependent! #ifdef WIN32 inline int CPP_SpinLock::TestAndSet(int* pTargetAddress, int nValue) { __asm { mov edx, dword ptr [pTargetAddress] mov eax, nValue lock xchg eax, dword ptr [edx] } // mov = 1 CPU cycle // lock = 1 CPU cycle // xchg = 3 CPU cycles } #endif // WIN32

Por cierto: aquí está la URL del código que publiqué: http://www.codeproject.com/KB/threads/spinlocks.aspx


Lo que dicen las otras respuestas acerca de que es imposible predecir con precisión el rendimiento del código que se ejecuta en una CPU moderna es cierto, pero eso no significa que las latencias sean desconocidas, o que conocerlas sea inútil.

Las latencias exactas para Intels y los procesadores de AMD se enumeran en las tablas de instrucciones de Agner Fog . Consulte también el Manual de referencia de optimización de arquitecturas Intel® 64 e IA-32 , y las latencias de instrucciones y el rendimiento para los procesadores AMD e Intel x86 (a partir de la respuesta de solo borrado de Can Berk Güder). AMD también tiene manuales en PDF en su propio sitio web con sus valores oficiales.

Para (micro) optimizar bucles apretados, conocer las latencias de cada instrucción puede ayudar mucho al programar su código manualmente. El programador puede hacer muchas optimizaciones que el compilador no puede (porque el compilador no puede garantizar que no cambiará el significado del programa).

Por supuesto, esto todavía requiere que conozcas muchos otros detalles sobre la CPU, como qué tan profundamente se canaliza, cuántas instrucciones puede emitir por ciclo, número de unidades de ejecución, etc. Y, por supuesto, estos números varían para diferentes CPU. Pero a menudo se puede llegar a un promedio razonable que más o menos funciona para todas las CPU.

Vale la pena señalar, sin embargo, que es mucho trabajo optimizar incluso unas pocas líneas de código en este nivel. Y es fácil hacer algo que resulta ser una pesimismo. Las CPU modernas son muy complicadas, y hacen un gran esfuerzo para obtener un buen rendimiento del código incorrecto. Pero también hay casos en que no son capaces de manejarlos de manera eficiente, o en los que cree que es inteligente y hace un código eficiente, y resulta que ralentiza la CPU.

Editar Mirando en el manual de optimización de Intel, tabla C-13: La primera columna es tipo de instrucción, luego hay una cantidad de columnas para latencia para cada CPUID. El CPUID indica a qué familia de procesadores se aplican los números, y se explican en otra parte del documento. La latencia especifica cuántos ciclos tarda antes de que el resultado de la instrucción esté disponible, por lo que este es el número que está buscando.

Las columnas de rendimiento muestran cuántas de este tipo de instrucciones se pueden ejecutar por ciclo.

Al buscar xchg en esta tabla, vemos que dependiendo de la familia de la CPU, toma 1-3 ciclos, y un mov toma 0.5-1. Estos son para los formularios de registro de registro de las instrucciones, no para un lock xchg con memoria, que es mucho más lento. Y, lo que es más importante, la latencia enormemente variable y el impacto en el código circundante (mucho más lento cuando existe una disputa con otro núcleo), por lo que mirar solo al mejor de los casos es un error. (No he buscado lo que significa cada CPUID, pero supongo que los .5 son para Pentium 4, que ejecutó algunos componentes del chip a doble velocidad, lo que le permite hacer las cosas en medio ciclo)

Sin embargo, realmente no veo para qué piensas utilizar esta información, pero si conoces la familia exacta de la CPU en la que se está ejecutando el código, sumar la latencia te indica el número mínimo de ciclos necesarios para ejecutar esta secuencia de instrucciones. .


Medir y contar ciclos de CPU ya no tiene sentido en el x86.

En primer lugar, pregúntese por qué CPU está contando ciclos? Core-2? un Athlon? Pentium-M? ¿Átomo? Todas estas CPU ejecutan código x86 pero todas tienen diferentes tiempos de ejecución. La ejecución incluso varía entre distintas versiones de la misma CPU.

El último x86 donde el conteo cíclico tenía sentido era el Pentium-Pro.

Además, tenga en cuenta que dentro de la CPU, la mayoría de las instrucciones se transcodifican en microcódigo y se ejecutan fuera de servicio por una unidad de ejecución interna que ni remotamente se parece a una x86. El rendimiento de una sola instrucción de CPU depende de la cantidad de recursos disponibles en la unidad de ejecución interna.

Por lo tanto, el tiempo para una instrucción depende no solo de la instrucción en sí sino también del código circundante.

De todos modos: puede estimar el uso de recursos de rendimiento y la latencia de las instrucciones para diferentes procesadores. La información relevante se puede encontrar en los sitios de Intel y AMD.

Agner Fog tiene un muy buen resumen en su sitio web. Consulte las tablas de instrucciones para conocer la latencia, el rendimiento y el conteo de uop. Vea el PDF de microarchictecture para aprender cómo interpretarlos.

http://www.agner.org/optimize

Pero tenga en cuenta que xchg -with-memory no tiene un rendimiento predecible, incluso si mira solo un modelo de CPU. Incluso en el caso de no contención con la línea de caché que ya está activa en la memoria caché L1D, ser una barrera de memoria completa significará que su impacto depende mucho de las cargas y almacena en otras direcciones en el código circundante.

Por cierto, dado que su código de ejemplo es una estructura básica de una estructura de datos sin bloqueo: ¿Ha considerado usar las funciones integradas del compilador? En win32 puede incluir intrin.h y usar funciones como _InterlockedExchange.

Eso te dará un mejor tiempo de ejecución porque el compilador puede alinear las instrucciones. El ensamblador en línea siempre obliga al compilador a deshabilitar las optimizaciones en torno al código asm.


Teniendo en cuenta la canalización, el procesamiento fuera de servicio, el microcódigo, los procesadores multinúcleo, etc. no hay garantía de que una determinada sección del código ensamblador tome exactamente x ciclos de CPU / ciclos de reloj / lo que sea.

Si existe tal referencia, solo podrá proporcionar generalizaciones generales dada una arquitectura particular, y dependiendo de cómo se implemente el microcódigo, puede encontrar que el Pentium M es diferente al Core 2 Duo que es diferente al AMD dual core , etc.

Tenga en cuenta que este artículo fue actualizado en 2000 y escrito anteriormente. Incluso el Pentium 4 es difícil de precisar con respecto al tiempo de instrucción: PIII, PII y el pentium original eran más fáciles, y los textos a los que se hace referencia probablemente se basaron en aquellos procesadores anteriores que tenían un tiempo de instrucción más bien definido.

En estos días, las personas generalmente usan el análisis estadístico para estimar el tiempo del código.


bloquear xchg eax, dword ptr [edx]

Tenga en cuenta que el bloqueo bloqueará la memoria de la recuperación de memoria para todos los núcleos, esto puede llevar 100 ciclos en algunos núcleos múltiples y también será necesario purgar una línea de caché. También parará la tubería. Entonces no me preocuparía por el resto.

Entonces, el rendimiento óptimo vuelve a sintonizar las regiones críticas de sus algoritmos.

Tenga en cuenta que en un solo núcleo puede optimizar esto quitando el bloqueo, pero es necesario para múltiples núcleos.