2011-12-08 47 views
14

he estado escribiendo en ensamblado x86 últimamente (por diversión) y me preguntaba si las instrucciones de cadena prefijadas rep tienen realmente una ventaja de rendimiento en procesadores modernos o si Recién implementado para compatibilidad con la espalda.Rendimiento de las instrucciones x86 rep en procesadores modernos (pipeline/superscalar)

entiendo por qué Intel habría implementado originalmente las instrucciones de rep cuando los procesadores solo ejecutaron una instrucción a la vez, pero ¿hay alguna ventaja de usarlas ahora?

Con un bucle que compila más instrucciones, hay más para llenar la tubería y/o emitirse fuera de servicio. ¿Los procesadores modernos están diseñados para optimizar estas instrucciones con prefijo de rep, o las instrucciones de rep se utilizan tan raramente en el código moderno que no son importantes para los fabricantes?

+0

No he estudiado esto en, por ejemplo, 5 años, pero en aquel momento mi experiencia personal era que, al menos, rep movsd y rep stosd eran más rápidos que un simple bucle, mientras que algunas de las variantes de exploración no. Sin embargo, eso podría haber cambiado significativamente desde entonces. –

+0

Realice una prueba en diferentes procesadores y compruébelo usted mismo. –

+0

Gracias por la entrada, muchachos. Alex: probablemente lo haga eventualmente, pero no tengo muchos procs diferentes para probarlo, así que sería solo en un proc real vs. un emulador que no tendría una interconexión. Además, soy flojo y preferiría no hacer ese trabajo si alguien más ya lo hubiera hecho. :) – RyanS

Respuesta

33

En AMD y en las guías de optimización de Intel se da mucho espacio para preguntas como esta. Validez de los consejos que se dan en esta zona tiene una "vida media" - diferentes generaciones de CPU se comportan de manera diferente, por ejemplo:

El Intel Architecture Optimization Manual da cifras de comparación de rendimiento para diversas técnicas de copia de bloque (incluyendo rep stosd) en la Tabla 7-2. Rendimiento relativo de rutinas de copia de memoria, pg. 7-37f., Para diferentes CPU, y nuevamente lo que es más rápido en uno podría no ser el más rápido en otros.

En muchos casos, las CPU x86 recientes (que tienen las operaciones de "cadena" SSE4.2) pueden realizar operaciones de cadena a través de la unidad SIMD, consulte this investigation.

Para realizar un seguimiento de todo esto (y/o mantenerse actualizado cuando las cosas cambian de nuevo, inevitablemente), lea Agner Fog's Optimization guides/blogs.

+0

+1 Excelente respuesta. El sitio de Agner Fog también tiene una gran cantidad de información valiosa. –

+4

+1 por mencionar Agner Fog – hirschhornsalz

+0

'rep movs' y' rep stos' son usualmente buenos (para un buffer alineado de mediano a grande), 'repe/repne scas/cmps' usualmente no son buenos. –

8

Además de la excelente respuesta de FrankH; Me gustaría señalar que el mejor método también depende de la longitud de la cadena, su alineación y si la longitud es fija o variable.

Para cadenas pequeñas (tal vez hasta aproximadamente 16 bytes) hacerlo manualmente con instrucciones simples es probablemente más rápido, ya que evita los costos de configuración de técnicas más complejas (y para cadenas de tamaño fijo se pueden desenrollar fácilmente). Para cadenas de tamaño medio (quizás de 16 bytes a 4 KiB), es probable que algo como "REP MOVSD" (con algunas instrucciones "MOVSB" incluidas si es posible una desalineación) sea lo mejor.

Para algo más grande que eso, algunas personas estarían tentadas de entrar en SSE/AVX y de captación previa, etc. Una mejor idea es arreglar la (s) llamada (s) para que no se copie (o strlen() o lo que sea) necesario en primer lugar. Si lo intentas lo suficiente, casi siempre encontrarás la manera. Nota: También tenga mucho cuidado con las "supuestas" rutinas rápidas de mempcy(); por lo general, se han probado en cadenas masivas y no se han probado en cadenas muy pequeñas/pequeñas/medianas.

También tenga en cuenta que (a los efectos de la optimización en lugar de conveniencia) debido a todas estas diferencias (probablemente longitud, alineación, fijo o variable tamaño, tipo de CPU, etc) la idea de tener una de usos múltiples "memcpy () "para todos los casos muy diferentes es miope.

+2

Ack. Las guías de optimización (tanto Intel/AMDs como los materiales de Agner Fog y muchos otros) sí mencionan estas cosas también; en muchos casos, una estrategia: 1. para cadenas cortas, instrucciones primitivas en línea 2.para tamaños medios, 'rep movs' de tamaño de operando grande 3. para bloques grandes conocidos, use las unidades SIMD. Y siempre pruebe en _sus_ datos, ya que el rendimiento 'ultra rápido VVX' se descompondrá si la mayoría de sus cadenas son <8 Bytes. –

+0

IIRC 'REP MOVSD' es, en hardware moderno, a menudo * mucho más lento * que' REP MOVSB'. Probablemente porque las CPU modernas tienen optimizaciones especiales solo para 'REP MOVSB', porque se usan mucho más a menudo que' REP MOVSD'. –

+0

@PaulGroke: Hay tal vez un par de CPU donde 'rep movsb' es mejor que' rep movsd', pero la mayoría implementa toda la magia de ERMSB para 'rep movsd' /' movsq' también. Y 'rep movsb' usualmente era * peor * en las CPU de Intel antes de la función Enhanved Rep MovSB de IvyBridge. Consulte [MOVSB ​​REP mejorado para memcpy] (https://stackoverflow.com/questions/43343231/enhanced-rep-movsb-for-memcpy), que tiene una respuesta * excelente * con muchos detalles sobre el ancho de banda de la memoria x86. –

0

Dado que nadie te ha dado ningún número, te daré algunos que he encontrado mediante la evaluación comparativa de mi recolector de basura que es muy pesado. Mis objetos para copiar son 60% 16 bytes de longitud y el resto 30% son 500 - 8000 bytes más o menos.

  • Condición previa: Tanto dst, src y n son múltiplos de 8.
  • Procesador: AMD Phenom (tm) II X6 1090T procesador de 64 bits/Linux

Aquí están mis tres memcpy variantes:

Hand-codificado bucle while:

if (n == 16) { 
    *dst++ = *src++; 
    *dst++ = *src++; 
} else { 
    size_t n_ptrs = n/sizeof(ptr); 
    ptr *end = dst + n_ptrs; 
    while (dst < end) { 
     *dst++ = *src++; 
    } 
} 

(ptr es un alias de uintptr_t). Tiempo: 101.16%

rep movsb

if (n == 16) { 
    *dst++ = *src++; 
    *dst++ = *src++; 
} else { 
    asm volatile("cld\n\t" 
       "rep ; movsb" 
       : "=D" (dst), "=S" (src) 
       : "c" (n), "D" (dst), "S" (src) 
       : "memory"); 
} 

tiempo: 103.22%

rep movsq

if (n == 16) { 
    *dst++ = *src++; 
    *dst++ = *src++; 
} else { 
    size_t n_ptrs = n/sizeof(ptr); 
    asm volatile("cld\n\t" 
       "rep ; movsq" 
       : "=D" (dst), "=S" (src) 
       : "c" (n_ptrs), "D" (dst), "S" (src) 
       : "memory"); 
} 

tiempo: 100.00%

req movsq gana por un pequeño margen.

+1

El registro de RCX también lo cambia REP MOVS. –

Cuestiones relacionadas