Enhanced REP MOVSB ​​para memcpy

Me gustaría utilizar REP MOVSB ​​(ERMSB) mejorado para obtener un ancho de banda alto para una memcpy personalizada.

ERMSB se introdujo con la microarchitecture Ivy Bridge. Consulte la sección “Operación REP MOVSB ​​y STOSB mejorada (ERMSB)” en el manual de optimización de Intel si no sabe qué es ERMSB.

La única forma en que sé hacer esto directamente es con el ensamblaje en línea. Obtuve la siguiente función en https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE

 static inline void *__movsb(void *d, const void *s, size_t n) { asm volatile ("rep movsb" : "=D" (d), "=S" (s), "=c" (n) : "0" (d), "1" (s), "2" (n) : "memory"); return d; } 

Sin embargo, cuando uso esto, el ancho de banda es mucho menor que con memcpy . __movsb obtiene 15 GB / sy memcpy obtiene 26 GB / s con mi sistema i7-6700HQ (Skylake), Ubuntu 16.10, DDR4 @ 2400 MHz de doble canal 32 GB, GCC 6.2.

¿Por qué el ancho de banda es mucho más bajo con REP MOVSB ? ¿Qué puedo hacer para mejorarlo?

Aquí está el código que usé para probar esto.

 //gcc -O3 -march=native -fopenmp foo.c #include  #include  #include  #include  #include  #include  static inline void *__movsb(void *d, const void *s, size_t n) { asm volatile ("rep movsb" : "=D" (d), "=S" (s), "=c" (n) : "0" (d), "1" (s), "2" (n) : "memory"); return d; } int main(void) { int n = 1<<30; //char *a = malloc(n), *b = malloc(n); char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096); memset(a,2,n), memset(b,1,n); __movsb(b,a,n); printf("%d\n", memcmp(b,a,n)); double dtime; dtime = -omp_get_wtime(); for(int i=0; i<10; i++) __movsb(b,a,n); dtime += omp_get_wtime(); printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime); dtime = -omp_get_wtime(); for(int i=0; i<10; i++) memcpy(b,a,n); dtime += omp_get_wtime(); printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime); } 

La razón por la que estoy interesado en rep movsb se basa en estos comentarios

Tenga en cuenta que en Ivybridge y Haswell, con búferes de gran tamaño para caber en MLC, puede vencer a movntdqa usando rep movsb; movntdqa incurre en RFO en LLC, rep movsb no … rep movsb es significativamente más rápido que movntdqa cuando se transmite a la memoria en Ivybridge y Haswell (¡pero ten en cuenta que antes de Ivybridge es lento!)

¿Qué falta / no es óptimo en esta implementación de memcpy?


Aquí están mis resultados en el mismo sistema de tinymembnech .

  C copy backwards : 7910.6 MB/s (1.4%) C copy backwards (32 byte blocks) : 7696.6 MB/s (0.9%) C copy backwards (64 byte blocks) : 7679.5 MB/s (0.7%) C copy : 8811.0 MB/s (1.2%) C copy prefetched (32 bytes step) : 9328.4 MB/s (0.5%) C copy prefetched (64 bytes step) : 9355.1 MB/s (0.6%) C 2-pass copy : 6474.3 MB/s (1.3%) C 2-pass copy prefetched (32 bytes step) : 7072.9 MB/s (1.2%) C 2-pass copy prefetched (64 bytes step) : 7065.2 MB/s (0.8%) C fill : 14426.0 MB/s (1.5%) C fill (shuffle within 16 byte blocks) : 14198.0 MB/s (1.1%) C fill (shuffle within 32 byte blocks) : 14422.0 MB/s (1.7%) C fill (shuffle within 64 byte blocks) : 14178.3 MB/s (1.0%) --- standard memcpy : 12784.4 MB/s (1.9%) standard memset : 30630.3 MB/s (1.1%) --- MOVSB copy : 8712.0 MB/s (2.0%) MOVSD copy : 8712.7 MB/s (1.9%) SSE2 copy : 8952.2 MB/s (0.7%) SSE2 nontemporal copy : 12538.2 MB/s (0.8%) SSE2 copy prefetched (32 bytes step) : 9553.6 MB/s (0.8%) SSE2 copy prefetched (64 bytes step) : 9458.5 MB/s (0.5%) SSE2 nontemporal copy prefetched (32 bytes step) : 13103.2 MB/s (0.7%) SSE2 nontemporal copy prefetched (64 bytes step) : 13179.1 MB/s (0.9%) SSE2 2-pass copy : 7250.6 MB/s (0.7%) SSE2 2-pass copy prefetched (32 bytes step) : 7437.8 MB/s (0.6%) SSE2 2-pass copy prefetched (64 bytes step) : 7498.2 MB/s (0.9%) SSE2 2-pass nontemporal copy : 3776.6 MB/s (1.4%) SSE2 fill : 14701.3 MB/s (1.6%) SSE2 nontemporal fill : 34188.3 MB/s (0.8%) 

Tenga en cuenta que en mi sistema, la SSE2 copy prefetched también es más rápida que la MOVSB copy .


En mis pruebas originales no desactivé turbo. Desactivé Turbo y probé de nuevo y no parece marcar una gran diferencia. Sin embargo, cambiar la administración de energía hace una gran diferencia.

Cuando lo hago

 sudo cpufreq-set -r -g performance 

A veces veo más de 20 GB / s con rep movsb .

con

 sudo cpufreq-set -r -g powersave 

lo mejor que veo es aproximadamente 17 GB / s. Pero memcpy no parece ser sensible a la administración de energía.


Comprobé la frecuencia (usando turbostat ) con y sin SpeedStep habilitado , con performance y con powersave para inactivo, una carga de 1 núcleo y una carga de 4 núcleos. Ejecuté la multiplicación de matriz densa MKL de Intel para crear una carga y establecer el número de subprocesos usando OMP_SET_NUM_THREADS . Aquí hay una tabla de los resultados (números en GHz).

  SpeedStep idle 1 core 4 core powersave OFF 0.8 2.6 2.6 performance OFF 2.6 2.6 2.6 powersave ON 0.8 3.5 3.1 performance ON 3.5 3.5 3.1 

Esto muestra que con el powersave incluso con SpeedStep desactivado, la CPU aún powersave la frecuencia inactiva de 0.8 GHz . Solo con el performance sin SpeedStep, la CPU funciona a una frecuencia constante.

Utilicé, por ejemplo, sudo cpufreq-set -r performance (porque cpufreq-set estaba dando resultados extraños) para cambiar la configuración de energía. Esto enciende nuevamente el turbo, así que tuve que deshabilitar el turbo después.

Este es un tema muy cercano a mi corazón y las investigaciones recientes, así que lo analizaré desde algunos angularjs: historia, algunas notas técnicas (en su mayoría académicas), resultados de pruebas en mi caja y, finalmente, un bash de responder a su pregunta real de cuándo y dónde rep movsb podría tener sentido.

En parte, esta es una llamada para compartir resultados : si puedes ejecutar Tinymembench y compartir los resultados junto con los detalles de tu CPU y configuración de RAM, sería genial. Especialmente si tiene una configuración de 4 canales, una caja de Ivy Bridge, una caja de servidor, etc.

Historia y asesoramiento oficial

El historial de rendimiento de las instrucciones rápidas de copia de cadena ha sido un asunto de poca importancia, es decir, períodos de rendimiento estancado que se alternan con grandes actualizaciones que los pusieron en línea o incluso más rápidos que los enfoques de la competencia. Por ejemplo, hubo un salto en el rendimiento en Nehalem (enfocándose principalmente en los gastos generales de inicio) y nuevamente en Ivy Bridge (la mayor parte del rendimiento total de segmentación para copias grandes). Puede encontrar información de hace una década sobre las dificultades de implementar las instrucciones de rep movs de un ingeniero de Intel en este hilo .

Por ejemplo, en las guías que preceden a la introducción de Ivy Bridge, el consejo típico es evitarlas o usarlas con mucho cuidado 1 .

La guía actual (bueno, junio de 2016) tiene una variedad de consejos confusos y algo inconsistentes, como 2 :

La variante específica de la implementación se elige en el tiempo de ejecución en función del diseño de los datos, la alineación y el valor del contador (ECX). Por ejemplo, MOVSB ​​/ STOSB con el prefijo REP se debe usar con un valor de contador menor o igual a tres para obtener el mejor rendimiento.

Entonces, ¿para copias de 3 o menos bytes? No necesita un prefijo de rep para eso en primer lugar, ya que con una latencia de inicio de ~ 9 ciclos demandada es casi seguro que es mejor con un simple DWORD o QWORD mov con un poco de twiddling para enmascarar el inusitado bytes (o quizás con 2 byte explícitos, mov palabra si sabes que el tamaño es exactamente tres).

Ellos continúan para decir:

Las instrucciones de MOVE / STORE de cadena tienen granularidades de datos múltiples. Para un movimiento de datos eficiente, se prefieren las granularidades de datos más grandes. Esto significa que se puede lograr una mejor eficiencia descomponiendo un valor de contador arbitrario en un número de palabras dobles más movimientos de un solo byte con un valor de conteo menor o igual a 3.

Esto ciertamente parece incorrecto en el hardware actual con ERMSB donde rep movsb es al menos tan rápido o más rápido que las variantes movd o movq para copias grandes.

En general, esa sección (3.7.5) de la guía actual contiene una mezcla de consejos razonables y muy obsoletos. Este es el rendimiento común de los manuales de Intel, ya que se actualizan de forma incremental para cada architecture (y pretenden cubrir casi dos décadas de architectures, incluso en el manual actual), y las secciones antiguas a menudo no se actualizan para reemplazar o dar consejos condicionales eso no se aplica a la architecture actual.

A continuación, cubren ERMSB explícitamente en la sección 3.7.6.

No voy a repasar los consejos restantes de manera exhaustiva, pero resumiré las partes buenas en el “por qué usarlo” a continuación.

Otros reclamos importantes de la guía son que en Haswell, rep movsb se ha mejorado para usar operaciones de 256 bits internamente.

Consideraciones técnicas

Este es solo un resumen rápido de las ventajas y desventajas subyacentes que tienen las instrucciones del rep desde el punto de vista de la implementación .

Ventajas para los rep movs

  1. Cuando se emite una instrucción rep movs, la CPU sabe que se debe transferir un bloque entero de un tamaño conocido. Esto puede ayudarlo a optimizar la operación de una manera que no puede con instrucciones discretas, por ejemplo:

    • Evitar la solicitud RFO cuando sabe que se sobrescribirá toda la línea de caché.
    • Emitir solicitudes de captación previa de forma inmediata y exacta. La memcpy hardware hace un buen trabajo al detectar patrones similares a memcpy , pero todavía se necesita un par de lecturas para memcpy y “over-prefetch” muchas líneas de caché más allá del final de la región copiada. rep movsb sabe exactamente el tamaño de la región y puede rep movsb exactamente.
  2. Aparentemente, no hay garantía de que se rep movs entre las tiendas dentro de 3 rep movs una sola rep movs que pueden ayudar a simplificar el tráfico de coherencia y simplemente otros aspectos del movimiento de bloque, versus instrucciones mov simples que tienen que obedecer un orden de memoria bastante estricto 4 .

  3. En principio, la instrucción de rep movs podría aprovechar varios trucos arquitectónicos que no están expuestos en el ISA. Por ejemplo, las architectures pueden tener rutas de datos internas más amplias que el ISA expone 5 y las rep movs podrían usar internamente.

Desventajas

  1. rep movsb debe implementar una semántica específica que puede ser más fuerte que el requisito de software subyacente. En particular, memcpy prohíbe la superposición de regiones, por lo que puede ignorar esa posibilidad, pero rep movsb permite y debe producir el resultado esperado. En las implementaciones actuales, la mayoría afecta a la sobrecarga de inicio, pero probablemente no a un gran bloque de rendimiento. De forma similar, rep movsb debe admitir copias byte-granulares, incluso si lo está usando para copiar bloques grandes que son un múltiplo de alguna gran potencia de 2.

  2. El software puede tener información sobre la alineación, el tamaño de copia y posibles alias que no se pueden comunicar al hardware si se usa rep movsb . Los comstackdores a menudo pueden determinar la alineación de los bloques de memoria 6, por lo que pueden evitar gran parte del trabajo de inicio que las rep movs deben hacer en cada invocación.

Resultados de la prueba

Aquí hay resultados de pruebas para muchos métodos de copia diferentes de tinymembench en mi i7-6700HQ a 2.6 GHz (lástima que tenga la CPU idéntica, así que no tenemos un nuevo punto de datos …):

  C copy backwards : 8284.8 MB/s (0.3%) C copy backwards (32 byte blocks) : 8273.9 MB/s (0.4%) C copy backwards (64 byte blocks) : 8321.9 MB/s (0.8%) C copy : 8863.1 MB/s (0.3%) C copy prefetched (32 bytes step) : 8900.8 MB/s (0.3%) C copy prefetched (64 bytes step) : 8817.5 MB/s (0.5%) C 2-pass copy : 6492.3 MB/s (0.3%) C 2-pass copy prefetched (32 bytes step) : 6516.0 MB/s (2.4%) C 2-pass copy prefetched (64 bytes step) : 6520.5 MB/s (1.2%) --- standard memcpy : 12169.8 MB/s (3.4%) standard memset : 23479.9 MB/s (4.2%) --- MOVSB copy : 10197.7 MB/s (1.6%) MOVSD copy : 10177.6 MB/s (1.6%) SSE2 copy : 8973.3 MB/s (2.5%) SSE2 nontemporal copy : 12924.0 MB/s (1.7%) SSE2 copy prefetched (32 bytes step) : 9014.2 MB/s (2.7%) SSE2 copy prefetched (64 bytes step) : 8964.5 MB/s (2.3%) SSE2 nontemporal copy prefetched (32 bytes step) : 11777.2 MB/s (5.6%) SSE2 nontemporal copy prefetched (64 bytes step) : 11826.8 MB/s (3.2%) SSE2 2-pass copy : 7529.5 MB/s (1.8%) SSE2 2-pass copy prefetched (32 bytes step) : 7122.5 MB/s (1.0%) SSE2 2-pass copy prefetched (64 bytes step) : 7214.9 MB/s (1.4%) SSE2 2-pass nontemporal copy : 4987.0 MB/s 

Algunos puntos clave:

  • Los métodos rep movs son más rápidos que todos los otros métodos que no son “no temporales” 7 , y considerablemente más rápidos que los enfoques “C” que copian 8 bytes a la vez.
  • Los métodos “no temporales” son más rápidos, hasta en un 26% aproximadamente que los de rep movs , pero ese es un delta mucho más pequeño que el que informó (26 GB / s frente a 15 GB / s = ~ 73%).
  • Si no está utilizando tiendas no temporales, el uso de copias de 8 bytes desde C es casi tan bueno como la carga / almacenamiento SSE de 128 bits de ancho. Esto se debe a que un buen ciclo de copia puede generar suficiente presión de memoria para saturar el ancho de banda (por ejemplo, 2,6 GHz * 1 tienda / ciclo * 8 bytes = 26 GB / s para tiendas).
  • No hay algoritmos explícitos de 256 bits en tinymembench (excepto probablemente la memcpy “estándar”), pero probablemente no importe debido a la nota anterior.
  • El mayor rendimiento de los enfoques de tienda no temporales sobre los temporales es de aproximadamente 1.45x, que es muy similar al 1.5x que esperaría si NT elimina 1 de 3 transferencias (es decir, 1 lectura, 1 escritura para NT versus 2 lee, 1 escribe). Los enfoques de rep movs se encuentran en el medio.
  • La combinación de una latencia de memoria bastante baja y un ancho de banda modesto de 2 canales significa que este chip en particular es capaz de saturar su ancho de banda de memoria a partir de un hilo único, lo que cambia el comportamiento dramáticamente.
  • rep movsd parece usar la misma magia que rep movsb en este chip. Eso es interesante porque ERMSB solo se dirige explícitamente a las pruebas de movsb y anteriores en archivos anteriores con ERMSB show movsb funcionan mucho más rápido que movsd . Esto es principalmente académico ya que movsb es más general que movsd todos modos.

Haswell

Al observar los resultados de Haswell amablemente proporcionados por iwillnotexist en los comentarios, vemos las mismas tendencias generales (se extrajeron los resultados más relevantes):

  C copy : 6777.8 MB/s (0.4%) standard memcpy : 10487.3 MB/s (0.5%) MOVSB copy : 9393.9 MB/s (0.2%) MOVSD copy : 9155.0 MB/s (1.6%) SSE2 copy : 6780.5 MB/s (0.4%) SSE2 nontemporal copy : 10688.2 MB/s (0.3%) 

El enfoque rep movsb es aún más lento que el memcpy no temporal, pero solo en un 14% aquí (en comparación con ~ 26% en la prueba Skylake). La ventaja de las técnicas NT por encima de sus primos temporales es ahora ~ 57%, incluso un poco más que el beneficio teórico de la reducción del ancho de banda.

¿Cuándo deberías usar rep movs ?

Finalmente, una puñalada a su pregunta real: ¿cuándo o por qué debería usarla? Se basa en lo anterior e introduce algunas ideas nuevas. Desafortunadamente, no hay una respuesta simple: tendrá que intercambiar varios factores, incluidos algunos que probablemente ni siquiera sepa exactamente, como los desarrollos futuros.

Una nota de que la alternativa a rep movsb puede ser la memcpy libc optimizada (incluidas las copias incluidas en el comstackdor), o puede ser una versión de memcpy enrollada a memcpy . Algunos de los beneficios a continuación se aplican solo en comparación con una u otra de estas alternativas (por ejemplo, “simplicidad” ayuda contra una versión enrollada a mano, pero no contra memcpy ), pero algunos se aplican a ambos.

Restricciones en las instrucciones disponibles

En algunos entornos, existe una restricción en ciertas instrucciones o el uso de ciertos registros. Por ejemplo, en el kernel de Linux, el uso de registros SSE / AVX o FP generalmente no está permitido. Por lo tanto, la mayoría de las variantes optimizadas de memcpy no se pueden usar ya que dependen de los registros SSE o AVX, y en x86 se usa una copia simple basada en mov 64 bits. Para estas plataformas, el uso de rep movsb permite la mayor parte del rendimiento de una memcpy optimizada sin romper la restricción del código SIMD.

Un ejemplo más general podría ser el código que tiene que apuntar a muchas generaciones de hardware, y que no utiliza el despacho específico del hardware (por ejemplo, el uso de cpuid ). Aquí puede verse obligado a utilizar solo conjuntos de instrucciones anteriores, lo que excluye cualquier AVX, etc. rep movsb podría ser un buen enfoque aquí, ya que permite el acceso “oculto” a cargas y tiendas más amplias sin utilizar nuevas instrucciones. Si apuntas al hardware pre-ERMSB, tendrías que ver si el rendimiento del rep movsb es aceptable allí, aunque …

Pruebas futuras

Un buen aspecto de rep movsb es que, en teoría , puede aprovechar la mejora arquitectónica en las architectures futuras, sin cambios de fuente, que los movimientos explícitos no pueden. Por ejemplo, cuando se introdujeron rutas de datos de 256 bits, rep movsb pudo aprovecharlas (como afirma Intel) sin necesidad de realizar cambios en el software. El software que utiliza movimientos de 128 bits (que era óptimo antes de Haswell) debería modificarse y recomstackrse.

Por lo tanto, es tanto un beneficio de mantenimiento de software (no es necesario cambiar la fuente) como un beneficio para los binarios existentes (no es necesario implementar nuevos binarios para aprovechar la mejora).

La importancia de esto depende de su modelo de mantenimiento (p. Ej., Con qué frecuencia se implementan nuevos binarios en la práctica) y es muy difícil juzgar qué tan rápidas pueden ser estas instrucciones en el futuro. Al menos, Intel es una especie de guía de usos en esta dirección, al comprometerse con al menos un rendimiento razonable en el futuro ( 15.3.3.6 ):

REP MOVSB ​​y REP STOSB continuarán funcionando razonablemente bien en futuros procesadores.

Superposición con el trabajo posterior

Este beneficio no aparecerá en un punto de referencia de memcpy simple, por supuesto, que por definición no tiene un trabajo posterior que se superponga, por lo que la magnitud del beneficio debería medirse cuidadosamente en un escenario del mundo real. Aprovechar al máximo podría requerir la reorganización del código que rodea el memcpy .

Este beneficio es señalado por Intel en su manual de optimización (sección 11.16.3.4) y en sus palabras:

Cuando se sabe que el recuento es de al menos mil bytes o más, el uso de REP MOVSB ​​/ STOSB mejorado puede proporcionar otra ventaja para amortizar el costo del código que no consume. La heurística se puede entender utilizando un valor de Cnt = 4096 y memset () como ejemplo:

• Una implementación SIMD de 256 bits de memset () necesitará emitir / ejecutar 128 instancias de operación de almacenamiento de 32 bytes con VMOVDQA, antes de que las secuencias de instrucciones no consumidoras puedan llegar a la jubilación.

• Una instancia de REP STOSB mejorada con ECX = 4096 se decodifica como un flujo de microoperación prolongado proporcionado por hardware, pero se retira como una instrucción. Hay muchas operaciones store_data que deben completarse antes de que se pueda consumir el resultado de memset (). Debido a que la finalización de la operación de almacenamiento de datos se desvincula de la retirada de pedido de progtwig, una parte sustancial del flujo de código no consumido puede procesarse a través de la emisión / ejecución y retiro, esencialmente libre de costo si la secuencia no consumidora no compite para almacenar recursos de buffer.

Así que Intel está diciendo que después de todo, algunos uops han emitido el código después del rep movsb , pero mientras muchas tiendas todavía están en vuelo y el rep movsb en su totalidad no se ha retirado todavía, uops de las siguientes instrucciones puede hacer más progreso a través del maquinaria de orden de lo que podrían si ese código vino después de un ciclo de copia.

Los uops de un bucle de carga y tienda explícito deben retirarse por separado en el orden del progtwig. Eso tiene que pasar para dejar espacio en el ROB para seguir a los uops.

No parece haber mucha información detallada acerca de cómo funciona la instrucción microcoded muy larga, como el rep movsb , exactamente. No sabemos exactamente cómo las twigs de microcódigo solicitan una secuencia diferente de uops del secuenciador de microcódigos, o cómo se retiran los uops. Si los uops individuales no tienen que retirarse por separado, ¿tal vez toda la instrucción solo ocupe un lugar en el ROB?

Cuando el front-end que alimenta la maquinaria OoO ve una instrucción rep movsb en el caché uop, activa la ROM Microcode Sequencer (MS-ROM) para enviar microcode uops a la cola que alimenta la etapa de emisión / cambio de nombre. Probablemente no sea posible que otros uops se mezclen con eso y emitan / ejecuten 8 mientras que rep movsb todavía está emitiendo, pero las instrucciones subsiguientes pueden buscarse / decodificarse y emitirse inmediatamente después de la última rep movsb uop, mientras que algunas de las copias hasn Aún no se ejecutó. Esto solo es útil si al menos parte de su código posterior no depende del resultado de memcpy (lo cual no es inusual).

Ahora, el tamaño de este beneficio es limitado: a lo sumo puede ejecutar N instrucciones (uops en realidad) más allá de la lenta instrucción rep movsb , en cuyo punto se detendrá, donde N es el tamaño ROB . Con los tamaños de ROB actuales de ~ 200 (192 en Haswell, 224 en Skylake), eso es un beneficio máximo de ~ 200 ciclos de trabajo gratuito para el código posterior con un IPC de 1. En 200 ciclos puede copiar alrededor de 800 bytes a 10 GB / s, por lo que para copias de ese tamaño puede obtener trabajo gratuito cerca del costo de la copia (de manera que la copia sea gratuita).

Sin embargo, a medida que el tamaño de las copias aumenta mucho, la importancia relativa de esto disminuye rápidamente (por ejemplo, si está copiando 80 KB, el trabajo gratuito es solo el 1% del costo de la copia). Aún así, es bastante interesante para copias de tamaño modesto.

Los bucles de copia no bloquean totalmente las instrucciones subsiguientes para su ejecución. Intel no entra en detalles sobre el tamaño del beneficio, o sobre qué tipo de copias o códigos circundantes hay más beneficio. (Código de alta latencia o ILP con alta o baja temperatura de origen o destino, frío o caliente, ILP alto o ILP bajo).

Tamaño del código

El tamaño del código ejecutado (unos pocos bytes) es microscópico en comparación con una rutina de memcpy optimizada típica. Si el rendimiento está limitado por i-cache (incluido el caché uop), el tamaño reducido del código podría ser beneficioso.

De nuevo, podemos vincular la magnitud de este beneficio en función del tamaño de la copia. En realidad no lo resolveré numéricamente, pero la intuición es que reducir el tamaño del código dynamic por B bytes puede ahorrar como máximo C * B cache-miss, para algunas constantes C. Cada llamada a memcpy incurre en el costo de falta de caché (o beneficio) una vez, pero la ventaja de mayor rendimiento se basa en el número de bytes copiados. Por lo tanto, para transferencias grandes, un mayor rendimiento dominará los efectos de la memoria caché.

De nuevo, esto no es algo que se mostrará en un punto de referencia simple, donde todo el ciclo sin duda encajará en el caché uop. Necesitará una prueba real en el lugar para evaluar este efecto.

Optimización específica de la architecture

Informó que en su hardware, rep movsb era considerablemente más lento que la plataforma memcpy . Sin embargo, incluso aquí hay informes del resultado opuesto en hardware anterior (como Ivy Bridge).

Eso es completamente plausible, ya que parece que las operaciones de movimiento de cuerdas reciben amor periódicamente, pero no en todas las generaciones, por lo que puede ser más rápido o al menos atado (en este punto puede ganar en base a otras ventajas) en las architectures donde ha estado actualizado, solo para quedarse atrás en el hardware posterior.

Citando a Andy Glew, quien debería saber una cosa o dos sobre esto después de implementar esto en el P6:

La gran debilidad de hacer cadenas rápidas en microcódigo fue […] que el microcódigo se desconectó de todas las generaciones, cada vez más lento hasta que alguien solucionó el problema. Al igual que una copia de hombres de la biblioteca se desentona. Supongo que es posible que una de las oportunidades perdidas fue utilizar cargas y tiendas de 128 bits cuando estuvieron disponibles, y así sucesivamente.

En ese caso, se puede ver como otra optimización “específica de la plataforma” para aplicar en las típicas rutinas memcpy cada truco en el libro que se encuentran en bibliotecas estándar y comstackdores JIT: pero solo para uso en architectures donde es mejor. Para material JIT o comstackdo AOT esto es fácil, pero para binarios comstackdos estáticamente esto requiere despacho específico de plataforma, pero eso a menudo ya existe (a veces implementado en tiempo de enlace), o el argumento mtune se puede usar para tomar una decisión estática.

Sencillez

Incluso en Skylake, donde parece haberse quedado atrás con las técnicas no temporales más rápidas absolutas, sigue siendo más rápido que la mayoría de los enfoques y es muy simple . Esto significa menos tiempo en validación, menos errores misteriosos, menos ajuste de tiempo y actualización de una implementación de memcpy monstruo (o, a la inversa, menos dependencia de los caprichos de los implementadores de biblioteca estándar si confía en eso).

Latency Bound Platforms

Los algoritmos de límite de rendimiento de memoria 9 pueden realmente estar funcionando en dos regímenes generales principales: límite de ancho de banda de DRAM o límite de concurrencia / latencia.

El primer modo es con el que probablemente esté familiarizado: el subsistema DRAM tiene un cierto ancho de banda teórico que puede calcular con bastante facilidad en función del número de canales, velocidad / ancho de datos y frecuencia. Por ejemplo, mi sistema DDR4-2133 con 2 canales tiene un ancho de banda máximo de 2.133 * 8 * 2 = 34.1 GB / s, igual que el reportado en ARK .

No mantendrá más que ese índice de DRAM (y generalmente algo menos debido a diversas ineficiencias) agregado en todos los núcleos del socket (es decir, es un límite global para sistemas de un solo socket).

El otro límite se impone por la cantidad de solicitudes concurrentes que un núcleo realmente puede emitir al subsistema de memoria. Imagínese si un núcleo solo pudiera tener 1 solicitud en progreso a la vez, para una línea de caché de 64 bytes: cuando la solicitud se completara, podría emitir otra. Supongamos también una latencia de memoria de 50ns muy rápida. Entonces, a pesar del gran ancho de banda DRAM de 34.1 GB / s, en realidad solo obtendría 64 bytes / 50 ns = 1.28 GB / s, o menos del 4% del ancho de banda máximo.

En la práctica, los núcleos pueden emitir más de una solicitud a la vez, pero no un número ilimitado. Por lo general, se entiende que solo hay 10 búferes de relleno de línea por núcleo entre L1 y el rest de la jerarquía de memoria, y tal vez 16 búferes de relleno entre L2 y DRAM. La captura previa compite por los mismos recursos, pero al menos ayuda a reducir la latencia efectiva. Para obtener más detalles, consulte cualquiera de los excelentes artículos escritos por el Dr. Bandwidth sobre el tema , principalmente en los foros de Intel.

Aún así, las CPU más recientes están limitadas por este factor, no por el ancho de banda de RAM. Por lo general, alcanzan 12-20 GB / s por núcleo, mientras que el ancho de banda de RAM puede ser de más de 50 GB / s (en un sistema de 4 canales). Solo algunos núcleos de “clientes” de 2 canales de generación reciente, que parecen tener un mejor puntaje, quizás más memorias intermedias de línea pueden alcanzar el límite de DRAM en un solo núcleo, y nuestros chips Skylake parecen ser uno de ellos.

Ahora, por supuesto, hay una razón por la que Intel diseña sistemas con 50 GB / s de ancho de banda DRAM, mientras que solo debe mantener <20 GB / s por núcleo debido a los límites de concurrencia: el límite anterior es de todo el socket y el último es por núcleo. Por lo tanto, cada núcleo en un sistema de 8 núcleos puede enviar solicitudes de 20 GB / s, momento en el que volverán a estar limitadas por DRAM.

¿Por qué estoy hablando y hablando sobre esto? Debido a que la mejor implementación de memcpy menudo depende del régimen en el que está operando. Una vez que tenga DRAM BW limitado (como aparentemente lo son nuestros chips, pero la mayoría no están en un único núcleo), el uso de escrituras no temporales es muy importante ya que ahorra la lectura por propiedad que normalmente desperdicia 1/3 de su ancho de banda. Verá eso exactamente en los resultados de prueba anteriores: las implementaciones de memcpy que no usan almacenes NT pierden 1/3 de su ancho de banda.

Sin embargo, si tiene una concurrencia limitada, la situación se iguala y, en ocasiones, se revierte. Usted tiene ancho de banda de DRAM, entonces las tiendas NT no ayudan y pueden incluso lastimar ya que pueden boost la latencia ya que el tiempo de transferencia para el buffer de línea puede ser más largo que un escenario donde prefetch trae la línea RFO a LLC (o incluso L2) y luego la tienda completa en LLC para una latencia más baja efectiva. Por último, el desmarque del servidor tiende a tener tiendas NT mucho más lentas que las del cliente (y gran ancho de banda), lo que acentúa este efecto.

Por lo tanto, en otras plataformas, es posible que las tiendas NT sean menos útiles (al menos cuando se preocupe por el rendimiento de un solo subproceso) y quizás rep movsb gane dónde (si obtiene lo mejor de ambos mundos).

Realmente, este último artículo es un llamado para la mayoría de las pruebas. Sé que las tiendas NT pierden su ventaja aparente para las pruebas de un único subproceso en la mayoría de los archs (incluidos los archs de servidor actuales), pero no sé cómo se comportará relativamente el rep movsb

Referencias

Otras buenas fonts de información no integradas en lo anterior.

comp.arch investigación de rep movsb versus alternativas. Muchas buenas notas sobre la predicción de bifurcaciones y una implementación del enfoque que he sugerido a menudo para bloques pequeños: utilizar la primera y / o la última lectura / escritura superpuestas en lugar de tratar de escribir solo exactamente el número requerido de bytes (por ejemplo, implementar todas las copias de 9 a 16 bytes como dos copias de 8 bytes que pueden superponerse en hasta 7 bytes).


1 Presumiblemente, la intención es restringirlo a los casos en que, por ejemplo, el tamaño del código es muy importante.

2 Consulte la Sección 3.7.5: Prefijo REP y movimiento de datos.

3 Es importante tener en cuenta que esto solo se aplica a las distintas tiendas dentro de la instrucción individual: una vez que se completa, el bloque de tiendas sigue apareciendo ordenado con respecto a las tiendas anteriores y siguientes. De modo que el código puede ver que las tiendas de los rep movs fuera de servicio entre sí, pero no con respecto a las tiendas anteriores o posteriores (y es la última garantía que generalmente necesita). Solo será un problema si usa el final del destino de la copia como un indicador de sincronización, en lugar de un almacén separado.

4 Tenga en cuenta que los almacenes discretos no temporales también evitan la mayoría de los requisitos de ordenamiento, aunque en la práctica el rep movs tiene aún más libertad ya que todavía hay algunas restricciones de ordenamiento en almacenes WC / NT.

5 Esto era común en la última parte de la era de 32 bits, donde muchos chips tenían rutas de datos de 64 bits (por ejemplo, para admitir FPU que tenían soporte para el tipo double 64 bits). Hoy, los chips “castrados” como las marcas Pentium o Celeron tienen AVX deshabilitado, pero presumiblemente el microcódigo rep movs puede seguir utilizando 256b cargas / tiendas.

6 Por ejemplo, debido a las reglas de alineación del lenguaje, los atributos u operadores de alineación, las reglas de aliasing u otra información determinada en tiempo de comstackción. En el caso de la alineación, incluso si no se puede determinar la alineación exacta, al menos podrán alzar las comprobaciones de alineación de los bucles o eliminar de otro modo las comprobaciones redundantes.

7 I’m making the assumption that “standard” memcpy is choosing a non-temporal approach, which is highly likely for this size of buffer.

8 That isn’t necessarily obvious, since it could be the case that the uop stream that is generated by the rep movsb simply monopolizes dispatch and then it would look very much like the explicit mov case. It seems that it doesn’t work like that however – uops from subsequent instructions can mingle with uops from the microcoded rep movsb .

9 Ie, those which can issue a large number of independent memory requests and hence saturate the available DRAM-to-core bandwidth, of which memcpy would be a poster child (and as apposed to purely latency bound loads such as pointer chasing).

Enhanced REP MOVSB (Ivy Bridge and later)

Ivy Bridge microarchitecture (processors released in 2012 and 2013) introduced Enhanced REP MOVSB (we still need to check the corresponding bit) and allowed us to copy memory fast.

Cheapest versions of later processors – Kaby Lake Celeron and Pentium, released in 2017, don’t have AVX that could have been used for fast memory copy, but still have the Enhanced REP MOVSB.

REP MOVSB (ERMSB) is only faster than AVX copy or general-use register copy if the block size is at least 256 bytes. For the blocks below 64 bytes, it is MUCH slower, because there is high internal startup in ERMSB – about 35 cycles.

See the Intel Manual on Optimization, section 3.7.6 Enhanced REP MOVSB and STOSB operation (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

  • startup cost is 35 cycles;
  • both the source and destination addresses have to be aligned to a 16-Byte boundary;
  • the source region should not overlap with the destination region;
  • the length have to be a multiple of 64 to produce higher performance;
  • the direction have to be forward (CLD).

As I said earlier, REP MOVSB begin to outperform other methods when the length is at least 256 bytes, but to see the clear benefit over AVX copy, the length have to be more than 2048 bytes.

On the effect of the alignment if REP MOVSB vs. AVX copy, the Intel Manual gives the following information:

  • if the source buffer is not aligned, the impact on ERMSB implementation versus 128-bit AVX is similar;
  • if the destination buffer is not aligned, the impact on ERMSB implementation can be 25% degradation, while 128-bit AVX implementation of memcpy may degrade only 5%, relative to 16-byte aligned scenario.

I have made tests on Intel Core i5-6600, under 64-bit, and I have compared REP MOVSB memcpy() with a simple MOV RAX, [SRC]; MOV [DST], RAX implementation when the data fits L1 cache :

REP MOVSB memcpy():

  - 1622400000 data blocks of 32 bytes took 17.9337 seconds to copy; 2760.8205 MB/s - 1622400000 data blocks of 64 bytes took 17.8364 seconds to copy; 5551.7463 MB/s - 811200000 data blocks of 128 bytes took 10.8098 seconds to copy; 9160.5659 MB/s - 405600000 data blocks of 256 bytes took 5.8616 seconds to copy; 16893.5527 MB/s - 202800000 data blocks of 512 bytes took 3.9315 seconds to copy; 25187.2976 MB/s - 101400000 data blocks of 1024 bytes took 2.1648 seconds to copy; 45743.4214 MB/s - 50700000 data blocks of 2048 bytes took 1.5301 seconds to copy; 64717.0642 MB/s - 25350000 data blocks of 4096 bytes took 1.3346 seconds to copy; 74198.4030 MB/s - 12675000 data blocks of 8192 bytes took 1.1069 seconds to copy; 89456.2119 MB/s - 6337500 data blocks of 16384 bytes took 1.1120 seconds to copy; 89053.2094 MB/s 

MOV RAX… memcpy():

  - 1622400000 data blocks of 32 bytes took 7.3536 seconds to copy; 6733.0256 MB/s - 1622400000 data blocks of 64 bytes took 10.7727 seconds to copy; 9192.1090 MB/s - 811200000 data blocks of 128 bytes took 8.9408 seconds to copy; 11075.4480 MB/s - 405600000 data blocks of 256 bytes took 8.4956 seconds to copy; 11655.8805 MB/s - 202800000 data blocks of 512 bytes took 9.1032 seconds to copy; 10877.8248 MB/s - 101400000 data blocks of 1024 bytes took 8.2539 seconds to copy; 11997.1185 MB/s - 50700000 data blocks of 2048 bytes took 7.7909 seconds to copy; 12710.1252 MB/s - 25350000 data blocks of 4096 bytes took 7.5992 seconds to copy; 13030.7062 MB/s - 12675000 data blocks of 8192 bytes took 7.4679 seconds to copy; 13259.9384 MB/s 

So, even on 128-bit blocks, REP MOVSB is slower than just a simple MOV RAX copy in a loop (not unrolled). The ERMSB implementation begins to outperform the MOV RAX loop only starting form 256-byte blocks.

Normal (not enhanced) REP MOVS on Nehalem and later

Surprisingly, previous architectures (Nehalem and later), that didn’t yet have Enhanced REP MOVB, had quite fast REP MOVSD/MOVSQ (but not REP MOVSB/MOVSW) implementation for large blocks, but not large enough to outsize the L1 cache.

Intel Optimization Manual (2.5.6 REP String Enhancement) gives the following information is related to Nehalem microarchitecture – Intel Core i5, i7 and Xeon processors released in 2009 and 2010.

REP MOVSB

The latency for MOVSB, is 9 cycles if ECX < 4; otherwise REP MOVSB with ECX > 9 have a 50-cycle startup cost.

  • tiny string (ECX < 4): the latency of REP MOVSB is 9 cycles;
  • small string (ECX is between 4 and 9): no official information in the Intel manual, probably more than 9 cycles but less than 50 cycles;
  • long string (ECX > 9): 50-cycle startup cost.

My conclusion: REP MOVSB is almost useless on Nehalem.

MOVSW/MOVSD/MOVSQ

Quote from the Intel Optimization Manual (2.5.6 REP String Enhancement):

  • Short string (ECX < = 12): the latency of REP MOVSW/MOVSD/MOVSQ is about 20 cycles.
  • Fast string (ECX >= 76: excluding REP MOVSB): the processor implementation provides hardware optimization by moving as many pieces of data in 16 bytes as possible. The latency of REP string latency will vary if one of the 16-byte data transfer spans across cache line boundary: = Split-free: the latency consists of a startup cost of about 40 cycles and each 64 bytes of data adds 4 cycles. = Cache splits: the latency consists of a startup cost of about 35 cycles and each 64 bytes of data adds 6 cycles.
  • Intermediate string lengths: the latency of REP MOVSW/MOVSD/MOVSQ has a startup cost of about 15 cycles plus one cycle for each iteration of the data movement in word/dword/qword.

Intel does not seem to be correct here. From the above quote we understand that for very large memory blocks, REP MOVSW is as fast as REP MOVSD/MOVSQ, but tests have shown that only REP MOVSD/MOVSQ are fast, while REP MOVSW is even slower than REP MOVSB on Nehalem and Westmere.

According to the information provided by Intel in the manual, on previous Intel microarchitectures (before 2008) the startup costs are even higher.

Conclusion: if you just need to copy data that fits L1 cache, just 4 cycles to copy 64 bytes of data is excellent, and you don’t need to use XMM registers!

REP MOVSD/MOVSQ is the universal solution that works excellent on all Intel processors (no ERMSB required) if the data fits L1 cache

Here are the tests of REP MOVS* when the source and destination was in the L1 cache, of blocks large enough to not be seriously affected by startup costs, but not that large to exceed the L1 cache size. Source: http://users.atw.hu/instlatx64/

Yonah (2006-2008)

  REP MOVSB 10.91 B/c REP MOVSW 10.85 B/c REP MOVSD 11.05 B/c 

Nehalem (2009-2010)

  REP MOVSB 25.32 B/c REP MOVSW 19.72 B/c REP MOVSD 27.56 B/c REP MOVSQ 27.54 B/c 

Westmere (2010-2011)

  REP MOVSB 21.14 B/c REP MOVSW 19.11 B/c REP MOVSD 24.27 B/c 

Ivy Bridge (2012-2013) – with Enhanced REP MOVSB

  REP MOVSB 28.72 B/c REP MOVSW 19.40 B/c REP MOVSD 27.96 B/c REP MOVSQ 27.89 B/c 

SkyLake (2015-2016) – with Enhanced REP MOVSB

  REP MOVSB 57.59 B/c REP MOVSW 58.20 B/c REP MOVSD 58.10 B/c REP MOVSQ 57.59 B/c 

Kaby Lake (2016-2017) – with Enhanced REP MOVSB

  REP MOVSB 58.00 B/c REP MOVSW 57.69 B/c REP MOVSD 58.00 B/c REP MOVSQ 57.89 B/c 

As you see, the implementation of REP MOVS differs significantly from one microarchitecture to another. On some processors, like Ivy Bridge – REP MOVSB is fastest, albeit just slightly faster than REP MOVSD/MOVSQ, but no doubt that on all processors since Nehalem, REP MOVSD/MOVSQ works very well – you even don’t need “Enhanced REP MOVSB”, since, on Ivy Bridge (2013) with Enhacnced REP MOVSB , REP MOVSD shows the same byte per clock data as on Nehalem (2010) without Enhacnced REP MOVSB , while in fact REP MOVSB became very fast only since SkyLake (2015) – twice as fast as on Ivy Bridge. So this Enhacnced REP MOVSB bit in the CPUID may be confusing – it only shows that REP MOVSB per se is OK, but not that any REP MOVS* is faster.

The most confusing ERMBSB implementation is on the Ivy Bridge microarchitecture. Yes, on very old processors, before ERMSB, REP MOVS* for large blocks did use a cache protocol feature that is not available to regular code (no-RFO). But this protocol is no longer used on Ivy Bridge that has ERMSB. According to Andy Glew’s comments on an answer to “why are complicated memcpy/memset superior?” from a Peter Cordes answer , a cache protocol feature that is not available to regular code was once used on older processors, but no longer on Ivy Bridge. And there comes an explanation of why the startup costs are so high for REP MOVS*: „The large overhead for choosing and setting up the right method is mainly due to the lack of microcode branch prediction”. There has also been an interesting note that Pentium Pro (P6) in 1996 implemented REP MOVS* with 64 bit microcode loads and stores and a no-RFO cache protocol – they did not violate memory ordering, unlike ERMSB in Ivy Bridge.

Renuncia

  1. This answer is only relevant for the cases where the source and the destination data fits L1 cache. Depending on circumstances, the particularities of memory access (cache, etc.) should be taken into consideration. Prefetch and NTI may give better results in certain cases, especially on the processors that didn’t yet have the Enhanced REP MOVSB. Even on these older processors, REP MOVSD might have used a cache protocol feature that is not available to regular code.
  2. The information in this answer is only related to Intel processors and not to the processors by other manufacturers like AMD that may have better or worse implementations of REP MOVS* instructions.
  3. I have presented test results for both SkyLake and Kaby Lake just for the sake of confirmation – these architectures have the same cycle-per-instruction data.
  4. All product names, trademarks and registered trademarks are property of their respective owners.

You say that you want:

an answer that shows when ERMSB is useful

But I’m not sure it means what you think it means. Looking at the 3.7.6.1 docs you link to, it explicitly says:

implementing memcpy using ERMSB might not reach the same level of throughput as using 256-bit or 128-bit AVX alternatives, depending on length and alignment factors.

So just because CPUID indicates support for ERMSB, that isn’t a guarantee that REP MOVSB will be the fastest way to copy memory. It just means it won’t suck as bad as it has in some previous CPUs.

However just because there may be alternatives that can, under certain conditions, run faster doesn’t mean that REP MOVSB is useless. Now that the performance penalties that this instruction used to incur are gone, it is potentially a useful instruction again.

Remember, it is a tiny bit of code (2 bytes!) compared to some of the more involved memcpy routines I have seen. Since loading and running big chunks of code also has a penalty (throwing some of your other code out of the cpu’s cache), sometimes the ‘benefit’ of AVX et al is going to be offset by the impact it has on the rest of your code. Depends on what you are doing.

You also ask:

Why is the bandwidth so much lower with REP MOVSB? What can I do to improve it?

It isn’t going to be possible to “do something” to make REP MOVSB run any faster. It does what it does.

If you want the higher speeds you are seeing from from memcpy, you can dig up the source for it. It’s out there somewhere. Or you can trace into it from a debugger and see the actual code paths being taken. My expectation is that it’s using some of those AVX instructions to work with 128 or 256bits at a time.

Or you can just… Well, you asked us not to say it.

This is not an answer to the stated question(s), only my results (and personal conclusions) when trying to find out.

In summary: GCC already optimizes memset() / memmove() / memcpy() (see eg gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() in the GCC sources; also look for stringop_algs in the same file to see architecture-dependent variants). So, there is no reason to expect massive gains by using your own variant with GCC (unless you’ve forgotten important stuff like alignment attributes for your aligned data, or do not enable sufficiently specific optimizations like -O2 -march= -mtune= ). If you agree, then the answers to the stated question are more or less irrelevant in practice.

(I only wish there was a memrepeat() , the opposite of memcpy() compared to memmove() , that would repeat the initial part of a buffer to fill the entire buffer.)


I currently have an Ivy Bridge machine in use (Core i5-6200U laptop, Linux 4.4.0 x86-64 kernel, with erms in /proc/cpuinfo flags). Because I wanted to find out if I can find a case where a custom memcpy() variant based on rep movsb would outperform a straightforward memcpy() , I wrote an overly complicated benchmark.

The core idea is that the main program allocates three large memory areas: original , current , and correct , each exactly the same size, and at least page-aligned. The copy operations are grouped into sets, with each set having distinct properties, like all sources and targets being aligned (to some number of bytes), or all lengths being within the same range. Each set is described using an array of src , dst , n triplets, where all src to src+n-1 and dst to dst+n-1 are completely within the current area.

A Xorshift* PRNG is used to initialize original to random data. (Like I warned above, this is overly complicated, but I wanted to ensure I’m not leaving any easy shortcuts for the compiler.) The correct area is obtained by starting with original data in current , applying all the triplets in the current set, using memcpy() provided by the C library, and copying the current area to correct . This allows each benchmarked function to be verified to behave correctly.

Each set of copy operations is timed a large number of times using the same function, and the median of these is used for comparison. (In my opinion, median makes the most sense in benchmarking, and provides sensible semantics — the function is at least that fast at least half the time.)

To avoid compiler optimizations, I have the program load the functions and benchmarks dynamically, at run time. The functions all have the same form, void function(void *, const void *, size_t) — note that unlike memcpy() and memmove() , they return nothing. The benchmarks (named sets of copy operations) are generated dynamically by a function call (that takes the pointer to the current area and its size as parameters, among others).

Unfortunately, I have not yet found any set where

 static void rep_movsb(void *dst, const void *src, size_t n) { __asm__ __volatile__ ( "rep movsb\n\t" : "+D" (dst), "+S" (src), "+c" (n) : : "memory" ); } 

would beat

 static void normal_memcpy(void *dst, const void *src, size_t n) { memcpy(dst, src, n); } 

using gcc -Wall -O2 -march=ivybridge -mtune=ivybridge using GCC 5.4.0 on aforementioned Core i5-6200U laptop running a linux-4.4.0 64-bit kernel. Copying 4096-byte aligned and sized chunks comes close, however.

This means that at least thus far, I have not found a case where using a rep movsb memcpy variant would make sense. It does not mean there is no such case; I just haven’t found one.

(At this point the code is a spaghetti mess I’m more ashamed than proud of, so I shall omit publishing the sources unless someone asks. The above description should be enough to write a better one, though.)


This does not surprise me much, though. The C compiler can infer a lot of information about the alignment of the operand pointers, and whether the number of bytes to copy is a compile-time constant, a multiple of a suitable power of two. This information can, and will/should, be used by the compiler to replace the C library memcpy() / memmove() functions with its own.

GCC does exactly this (see eg gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() in the GCC sources; also look for stringop_algs in the same file to see architecture-dependent variants). Indeed, memcpy() / memset() / memmove() has already been separately optimized for quite a few x86 processor variants; it would quite surprise me if the GCC developers had not already included erms support.

GCC provides several function attributes that developers can use to ensure good generated code. For example, alloc_align (n) tells GCC that the function returns memory aligned to at least n bytes. An application or a library can choose which implementation of a function to use at run time, by creating a “resolver function” (that returns a function pointer), and defining the function using the ifunc (resolver) attribute.

One of the most common patterns I use in my code for this is

 some_type *pointer = __builtin_assume_aligned(ptr, alignment); 

where ptr is some pointer, alignment is the number of bytes it is aligned to; GCC then knows/assumes that pointer is aligned to alignment bytes.

Another useful built-in, albeit much harder to use correctly , is __builtin_prefetch() . To maximize overall bandwidth/efficiency, I have found that minimizing latencies in each sub-operation, yields the best results. (For copying scattered elements to consecutive temporary storage, this is difficult, as prefetching typically involves a full cache line; if too many elements are prefetched, most of the cache is wasted by storing unused items.)

There are far more efficient ways to move data. These days, the implementation of memcpy will generate architecture specific code from the compiler that is optimized based upon the memory alignment of the data and other factors. This allows better use of non-temporal cache instructions and XMM and other registers in the x86 world.

When you hard-code rep movsb prevents this use of intrinsics.

Therefore, for something like a memcpy , unless you are writing something that will be tied to a very specific piece of hardware and unless you are going to take the time to write a highly optimized memcpy function in assembly (or using C level intrinsics), you are far better off allowing the compiler to figure it out for you.

As a general memcpy() guide:

a) If the data being copied is tiny (less than maybe 20 bytes) and has a fixed size, let the compiler do it. Reason: Compiler can use normal mov instructions and avoid the startup overheads.

b) If the data being copied is small (less than about 4 KiB) and is guaranteed to be aligned, use rep movsb (if ERMSB is supported) or rep movsd (if ERMSB is not supported). Reason: Using an SSE or AVX alternative has a huge amount of “startup overhead” before it copies anything.

c) If the data being copied is small (less than about 4 KiB) and is not guaranteed to be aligned, use rep movsb . Reason: Using SSE or AVX, or using rep movsd for the bulk of it plus some rep movsb at the start or end, has too much overhead.

d) For all other cases use something like this:

  mov edx,0 .again: pushad .nextByte: pushad popad mov al,[esi] pushad popad mov [edi],al pushad popad inc esi pushad popad inc edi pushad popad loop .nextByte popad inc edx cmp edx,1000 jb .again 

Reason: This will be so slow that it will force programmers to find an alternative that doesn’t involve copying huge globs of data; and the resulting software will be significantly faster because copying large globs of data was avoided.