c gcc assembly x86 memcpy

REP MOVSB mejorado para memcpy



gcc assembly (6)

REP MOVSB ​​mejorado (Ivy Bridge y posterior)

La microarquitectura Ivy Bridge (procesadores lanzados en 2012 y 2013) introdujo Enhanced REP MOVSB (todavía necesitamos verificar el bit correspondiente) y nos permitió copiar la memoria rápidamente.

Las versiones más baratas de los procesadores posteriores: Kaby Lake Celeron y Pentium, lanzadas en 2017, no tienen AVX que podría haberse usado para una copia rápida de la memoria, pero aún tienen el MOVSB ​​REP mejorado.

REP MOVSB ​​(ERMSB) es solo más rápido que la copia AVX o la copia de registro de uso general si el tamaño del bloque es de al menos 256 bytes. Para los bloques de menos de 64 bytes, es MUCHO más lento, porque hay un alto inicio interno en ERMSB, aproximadamente 35 ciclos.

Consulte el Manual de Intel sobre optimización, sección 3.7.6 Operación REP MOVSB ​​mejorada y STOSB (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

  • el costo inicial es de 35 ciclos;
  • tanto la dirección de origen como la de destino deben estar alineadas con un límite de 16 bytes;
  • la región de origen no debe superponerse con la región de destino;
  • la longitud debe ser un múltiplo de 64 para producir un mayor rendimiento;
  • la dirección tiene que ser hacia adelante (CLD).

Como dije anteriormente, REP MOVSB ​​comienza a superar a otros métodos cuando la longitud es de al menos 256 bytes, pero para ver el claro beneficio sobre la copia AVX, la longitud debe ser superior a 2048 bytes.

Sobre el efecto de la alineación si REP MOVSB ​​vs. AVX copia, el Manual de Intel brinda la siguiente información:

  • si el búfer de origen no está alineado, el impacto en la implementación de ERMSB versus AVX de 128 bits es similar;
  • si el búfer de destino no está alineado, el impacto en la implementación de ERMSB puede ser un 25% de degradación, mientras que la implementación AVX de 128 bits de memcpy puede degradar solo un 5%, en relación con el escenario alineado de 16 bytes.

He realizado pruebas en Intel Core i5-6600, de menos de 64 bits, y he comparado REP MOVSB ​​memcpy () con un simple MOV RAX, [SRC]; MOV [DST], implementación RAX cuando los datos se ajustan al caché L1 :

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

Entonces, incluso en bloques de 128 bits, REP MOVSB ​​es más lento que una simple copia MOV RAX en un bucle (no desenrollado). La implementación de ERMSB comienza a superar el bucle MOV RAX solo comenzando desde bloques de 256 bytes.

MOVS REP normales (no mejorados) en Nehalem y posteriores

Sorprendentemente, las arquitecturas anteriores (Nehalem y posteriores), que todavía no tenían REP MOVB mejorado, tenían una implementación REP MOVSD / MOVSQ bastante rápida (pero no REP MOVSB ​​/ MOVSW) para bloques grandes, pero no lo suficientemente grandes como para sobredimensionar el caché L1.

Intel Optimization Manual (2.5.6 REP String Enhancement) brinda la siguiente información relacionada con la microarquitectura Nehalem: procesadores Intel Core i5, i7 y Xeon lanzados en 2009 y 2010.

REP MOVSB

La latencia para MOVSB ​​es de 9 ciclos si ECX <4; de lo contrario, REP MOVSB ​​con ECX> 9 tiene un costo de inicio de 50 ciclos.

  • cadena pequeña (ECX <4): la latencia de REP MOVSB ​​es de 9 ciclos;
  • cadena pequeña (ECX es entre 4 y 9): no hay información oficial en el manual de Intel, probablemente más de 9 ciclos pero menos de 50 ciclos;
  • cadena larga (ECX> 9): costo de inicio de 50 ciclos.

Mi conclusión: REP MOVSB ​​es casi inútil en Nehalem.

MOVSW / MOVSD / MOVSQ

Cita del Manual de optimización de Intel (2.5.6 REP String Enhancement):

  • Cadena corta (ECX <= 12): la latencia de REP MOVSW / MOVSD / MOVSQ es de aproximadamente 20 ciclos.
  • Cadena rápida (ECX> = 76: excluyendo REP MOVSB): la implementación del procesador proporciona optimización de hardware al mover tantos datos en 16 bytes como sea posible. La latencia de la latencia de cadena REP variará si uno de los 16 bytes de transferencia de datos se extiende a través del límite de la línea de caché: = sin división: la latencia consiste en un costo de inicio de aproximadamente 40 ciclos y cada 64 bytes de datos agrega 4 ciclos. = Divisiones de caché: la latencia consiste en un costo de inicio de aproximadamente 35 ciclos y cada 64 bytes de datos agrega 6 ciclos.
  • Longitudes de cadena intermedias: la latencia de REP MOVSW / MOVSD / MOVSQ tiene un costo inicial de aproximadamente 15 ciclos más un ciclo por cada iteración del movimiento de datos en word / dword / qword.

Intel no parece ser correcto aquí. De la cita anterior entendemos que para bloques de memoria muy grandes, REP MOVSW es ​​tan rápido como REP MOVSD / MOVSQ, pero las pruebas han demostrado que solo REP MOVSD / MOVSQ son rápidos, mientras que REP MOVSW es ​​incluso más lento que REP MOVSB ​​en Nehalem y Westmere .

Según la información proporcionada por Intel en el manual, en microarquitecturas Intel anteriores (antes de 2008) los costos de inicio son aún más altos.

Conclusión: si solo necesita copiar datos que se ajusten a la caché L1, solo 4 ciclos para copiar 64 bytes de datos es excelente, ¡y no necesita usar registros XMM!

REP MOVSD / MOVSQ es la solución universal que funciona excelente en todos los procesadores Intel (no se requiere ERMSB) si los datos se ajustan al caché L1

Estas son las pruebas de REP MOVS * cuando el origen y el destino estaban en la caché L1, de bloques lo suficientemente grandes como para no verse seriamente afectados por los costos de inicio, pero no tan grandes como para exceder el tamaño de la caché L1. Fuente: 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) - con MOVSB ​​REP mejorado

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) - con MOVSB ​​REP mejorado

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) - con MOVSB ​​REP mejorado

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

Como puede ver, la implementación de REP MOVS difiere significativamente de una microarquitectura a otra. En algunos procesadores, como Ivy Bridge, REP MOVSB ​​es el más rápido, aunque solo un poco más rápido que REP MOVSD / MOVSQ, pero no hay duda de que en todos los procesadores ya que Nehalem, REP MOVSD / MOVSQ funciona muy bien, incluso no necesita "REP mejorado" MOVSB ​​", ya que, en Ivy Bridge (2013) con REP MOVSB ​​mejorado , REP MOVSD muestra los mismos datos de byte por reloj que en Nehalem (2010) sin REP MOVSB ​​mejorado , mientras que REP MOVSB ​​se volvió muy rápido solo desde SkyLake (2015) - El doble de rápido que en Ivy Bridge. Por lo tanto, este bit REV MOVSB ​​mejorado en el CPUID puede ser confuso: solo muestra que REP MOVSB per se está bien, pero no que ninguno REP MOVS* sea ​​más rápido.

La implementación de ERMBSB más confusa está en la microarquitectura Ivy Bridge. Sí, en procesadores muy antiguos, antes de ERMSB, REP MOVS * para bloques grandes usaba una función de protocolo de caché que no está disponible para el código normal (sin RFO). Pero este protocolo ya no se usa en Ivy Bridge que tiene ERMSB. Según los comentarios de Andy Glew sobre una respuesta a "¿por qué los memcpy / memset superiores son complicados?" de una respuesta de Peter Cordes , una característica del protocolo de caché que no está disponible para el código normal se usó una vez en procesadores más antiguos, pero ya no en Ivy Bridge. Y llega una explicación de por qué los costos de inicio son tan altos para REP MOVS *: "La gran sobrecarga para elegir y configurar el método correcto se debe principalmente a la falta de predicción de ramificaciones de microcódigo". También ha habido una nota interesante de que Pentium Pro (P6) implementó en 1996 REP MOVS * con cargas y almacenes de microcódigos de 64 bits y un protocolo de caché sin RFO: no violaron los pedidos de memoria, a diferencia de ERMSB en Ivy Bridge.

Renuncia

  1. Esta respuesta solo es relevante para los casos en que los datos de origen y destino se ajustan al caché L1. Según las circunstancias, se deben tener en cuenta las particularidades del acceso a la memoria (caché, etc.). Prefetch y NTI pueden dar mejores resultados en ciertos casos, especialmente en los procesadores que aún no tenían el MOVSB ​​REP mejorado. Incluso en estos procesadores más antiguos, REP MOVSD podría haber utilizado una función de protocolo de caché que no está disponible para el código normal.
  2. La información en esta respuesta solo está relacionada con los procesadores Intel y no con los procesadores de otros fabricantes como AMD que pueden tener implementaciones mejores o peores de las instrucciones REP MOVS *.
  3. He presentado los resultados de las pruebas tanto para SkyLake como para Kaby Lake solo como confirmación: estas arquitecturas tienen los mismos datos de ciclo por instrucción.
  4. Todos los nombres de productos, marcas comerciales y marcas registradas son propiedad de sus respectivos dueños.

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

ERMSB se introdujo con la microarquitectura Ivy Bridge. Consulte la sección "Funcionamiento mejorado de REP MOVSB ​​y STOSB (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 de 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 a 2400 MHz de doble canal de 32 GB, GCC 6.2.

¿Por qué el ancho de banda es mucho menor 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 <stdlib.h> #include <string.h> #include <stdio.h> #include <stddef.h> #include <omp.h> #include <x86intrin.h> 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 el rep movsb se basa en estos comentarios

Tenga en cuenta que en Ivybridge y Haswell, con buffers demasiado grandes para caber en MLC, puede vencer a movntdqa usando rep movsb; movntdqa incurre en una 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 / subó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é el turbo. Inhabilité turbo y probé nuevamente y no parece hacer mucha 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 de unos 17 GB / s. Pero memcpy no parece ser sensible a la administración de energía.

Verifiqué la frecuencia (usando el turbostat ) con y sin SpeedStep habilitado , con performance y con powersave de 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 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 deshabilitado, la CPU sigue powersave la frecuencia inactiva de 0.8 GHz . Solo con el performance sin SpeedStep la CPU funciona a una frecuencia constante.

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


Como memcpy() guía general :

a) Si los datos que se copian son pequeños (menos de quizás 20 bytes) y tienen un tamaño fijo, deje que el compilador lo haga. Motivo: el compilador puede usar mov instrucciones normales y evitar los gastos generales de inicio.

b) Si los datos que se copian son pequeños (menos de aproximadamente 4 KiB) y se garantiza que están alineados, use rep movsb (si ERMSB es compatible) o rep movsd (si ERMSB no es compatible). Motivo: el uso de una alternativa SSE o AVX tiene una gran cantidad de "gastos generales de inicio" antes de copiar nada.

c) Si los datos que se copian son pequeños (menos de aproximadamente 4 KiB) y no se garantiza que estén alineados, úselos rep movsb . Motivo: el uso de SSE o AVX, o el uso rep movsd de la mayor parte más algunos rep movsb al principio o al final, tiene demasiada sobrecarga.

d) Para todos los demás casos, use algo como esto:

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

Motivo: Esto será tan lento que obligará a los programadores a encontrar una alternativa que no implique copiar grandes cantidades de datos; y el software resultante será significativamente más rápido porque se evitó la copia de grandes cantidades de datos.


Dices que quieres:

una respuesta que muestra cuándo ERMSB es útil

Pero no estoy seguro de que signifique lo que crees que significa. Mirando los documentos 3.7.6.1 a los que se vincula, dice explícitamente:

implementar memcpy usando ERMSB podría no alcanzar el mismo nivel de rendimiento que usar alternativas AVX de 256 o 128 bits, dependiendo de la longitud y los factores de alineación.

Entonces, solo porque CPUID indica soporte para ERMSB, eso no es una garantía de que REP MOVSB ​​sea la forma más rápida de copiar memoria. Simplemente significa que no será tan malo como lo ha hecho en algunas CPU anteriores.

Sin embargo, el hecho de que pueda haber alternativas que, bajo ciertas condiciones, se ejecuten más rápido no significa que REP MOVSB ​​sea inútil. Ahora que las penalizaciones de rendimiento en las que solía incurrir esta instrucción han desaparecido, es potencialmente una instrucción útil nuevamente.

Recuerde, es un pequeño fragmento de código (¡2 bytes!) En comparación con algunas de las rutinas de memcpy más complicadas que he visto. Dado que cargar y ejecutar grandes fragmentos de código también tiene una penalización (arrojar algunos de sus otros códigos del caché de la CPU), a veces el ''beneficio'' de AVX y otros se verá compensado por el impacto que tiene en el resto de su código código. Depende de lo que estés haciendo.

También preguntas:

¿Por qué el ancho de banda es mucho menor con REP MOVSB? ¿Qué puedo hacer para mejorarlo?

No será posible "hacer algo" para hacer que REP MOVSB ​​se ejecute más rápido. Hace lo que hace.

Si desea las velocidades más altas que está viendo desde memcpy, puede desenterrar la fuente. Está ahí afuera en alguna parte. O puede rastrearlo desde un depurador y ver las rutas de código reales que se están tomando. Mi expectativa es que use algunas de esas instrucciones AVX para trabajar con 128 o 256 bits a la vez.

O simplemente puedes ... Bueno, nos pediste que no lo dijéramos.


Esta no es una respuesta a la (s) pregunta (s) establecida (s), solo mis resultados (y conclusiones personales) cuando trato de averiguarlo.

En resumen: GCC ya optimiza memset() / memmove() / memcpy() (consulte, por ejemplo, gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() en las fuentes de GCC; también busque stringop_algs en el mismo archivo para ver las variantes dependientes de la arquitectura). Por lo tanto, no hay razón para esperar ganancias masivas al usar su propia variante con GCC (a menos que haya olvidado cosas importantes como los atributos de alineación para sus datos alineados, o no habilite optimizaciones suficientemente específicas como -O2 -march= -mtune= ). Si está de acuerdo, las respuestas a la pregunta planteada son más o menos irrelevantes en la práctica.

(Solo desearía que hubiera un memrepeat() , al contrario de lo que se memcpy() compara memmove() , que repetiría la parte inicial de un búfer para llenar todo el búfer).

Actualmente tengo una máquina de Ivy Bridge en uso (portátil Core i5-6200U, Linux 4.4.0 kernel x86-64, la erms de /proc/cpuinfo las banderas). Debido a que quería averiguar si puedo encontrar un caso en el que una variante personalizada memcpy () basada en rep movsb un rendimiento sencillo memcpy() , escribí un punto de referencia demasiado complicado.

La idea central es que el programa principal asigna tres grandes áreas de memoria: original , current y correct , cada uno exactamente el mismo tamaño, y por lo menos alineamiento de página. Las operaciones de copia se agrupan en conjuntos, con cada conjunto con propiedades distintas, como todas las fuentes y destinos alineados (a un cierto número de bytes), o todas las longitudes dentro del mismo rango. Cada conjunto se describe utilizando una matriz de src , dst , n tríos, donde todos src a src+n-1 y dst a dst+n-1 son completamente dentro de la current zona.

Se Xorshift* un Xorshift* PRNG para inicializar original a datos aleatorios. (Como he advertido anteriormente, esto es demasiado complicado, pero quería asegurarse de que no estoy dejando ningún atajos fáciles para el compilador.) El correct área se obtiene a partir de original los datos en current , la aplicación de todos los grupos en el conjunto actual, utilizando memcpy() siempre por la biblioteca C, y copiando el current área a correct . Esto permite que cada función de referencia se verifique para que se comporte correctamente.

Cada conjunto de operaciones de copia se cronometra una gran cantidad de veces usando la misma función, y la mediana de estas se usa para la comparación. (En mi opinión, la mediana tiene más sentido en la evaluación comparativa y proporciona una semántica sensata: la función es al menos tan rápida al menos la mitad del tiempo).

Para evitar optimizaciones del compilador, hago que el programa cargue las funciones y los puntos de referencia dinámicamente, en tiempo de ejecución. Las funciones tienen todas la misma forma, void function(void *, const void *, size_t) - en cuenta que a diferencia memcpy() y memmove() , devuelven nada. Los puntos de referencia (conjuntos de operaciones de copia con nombre) se generan dinámicamente mediante una llamada de función (que lleva el puntero al current área y su tamaño como parámetros, entre otros).

Desafortunadamente, todavía no he encontrado ningún conjunto donde

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" ); }

golpearía

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

utilizando gcc -Wall -O2 -march=ivybridge -mtune=ivybridge GCC 5.4.0 en el portátil Core i5-6200U mencionado anteriormente con un kernel de 64 bits linux-4.4.0. Sin embargo, la copia de fragmentos alineados y de tamaño de 4096 bytes se acerca.

Esto significa que, al menos hasta ahora, no he encontrado un caso en el rep movsb que tenga sentido usar una variante de memcpy. No significa que no haya tal caso; Simplemente no he encontrado uno.

(En este punto, el código es un desastre de espagueti del que estoy más avergonzado que orgulloso, por lo que omitiré publicar las fuentes a menos que alguien pregunte. Sin embargo, la descripción anterior debería ser suficiente para escribir una mejor).

Sin embargo, esto no me sorprende mucho. El compilador de C puede inferir una gran cantidad de información sobre la alineación de los punteros de los operandos, y si el número de bytes para copiar es una constante de tiempo de compilación, un múltiplo de una potencia adecuada de dos. Esta información puede, y debe / debe, ser utilizada por el compilador para reemplazar la biblioteca memcpy() / memmove() funciones de C con la suya.

GCC hace exactamente esto (consulte, por ejemplo, gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() en las fuentes de GCC; también busque stringop_algs en el mismo archivo para ver las variantes dependientes de la arquitectura). De hecho, memcpy() / memset() / memmove() ya se ha optimizado por separado para bastantes variantes de procesador x86; me sorprendería bastante si los desarrolladores de GCC no hubieran incluido el soporte de erms.

GCC proporciona varios atributos de función que los desarrolladores pueden usar para garantizar un buen código generado. Por ejemplo, alloc_align (n) le dice a GCC que la función devuelve la memoria alineada al menos a n bytes. Una aplicación o una biblioteca puede elegir qué implementación de una función usar en tiempo de ejecución, creando una "función de resolución" (que devuelve un puntero de función) y definiendo la función usando el ifunc (resolver) atributo.

Uno de los patrones más comunes que uso en mi código para esto es

some_type *pointer = __builtin_assume_aligned(ptr, alignment);

donde ptr hay algún puntero, alignment es el número de bytes con el que está alineado; GCC entonces sabe / asume que pointer está alineado a alignment bytes.

Otro útil incorporado, aunque mucho más difícil de usar correctamente , es __builtin_prefetch() . Para maximizar el ancho de banda / eficiencia general, descubrí que minimizar las latencias en cada suboperación produce los mejores resultados. (Para copiar elementos dispersos en almacenamiento temporal consecutivo, esto es difícil, ya que la captación previa generalmente implica una línea de caché completa; si se captan demasiados elementos, la mayor parte de la caché se desperdicia almacenando elementos no utilizados).


Hay formas mucho más eficientes de mover datos. En estos días, la implementación de memcpy generará código específico de arquitectura del compilador que está optimizado en función de la alineación de la memoria de los datos y otros factores. Esto permite un mejor uso de las instrucciones de caché no temporales y XMM y otros registros en el mundo x86.

Cuando codificas duro rep movsb evita este uso de intrínsecos.

Por lo tanto, para algo como a memcpy , a menos que esté escribiendo algo que esté vinculado a una pieza de hardware muy específica y a menos que se tome el tiempo para escribir una memcpy función altamente optimizada en el ensamblaje (o utilizando intrínsecos de nivel C), está mucho mejor permitiendo que el compilador lo descubra por usted.


Este es un tema bastante cercano a mi corazón e investigaciones recientes, por lo que lo veré desde algunos ángulos: historia, algunas notas técnicas (principalmente académicas), resultados de las pruebas en mi caja y, finalmente, un intento de responder a su pregunta real. de cuándo y dónde el rep movsb podría tener sentido.

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

Historia y asesoramiento oficial

El historial de rendimiento de las instrucciones rápidas de copia de cadenas ha sido un poco un asunto de escalones, es decir, períodos de rendimiento estancado que se alternan con grandes actualizaciones que los alinearon o incluso más rápido que los enfoques competitivos. Por ejemplo, hubo un salto en el rendimiento en Nehalem (principalmente dirigido a los gastos generales de inicio) y nuevamente en Ivy Bridge (la mayoría de los objetivos de rendimiento total 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 advice típico es evitarlos o usarlos 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 tiempo de ejecución en función del diseño de datos, la alineación y el valor del contador (ECX). Por ejemplo, MOVSB ​​/ STOSB con el prefijo REP debe usarse con un valor de contador menor o igual a tres para obtener el mejor rendimiento.

Entonces, ¿para copias de 3 o menos bytes? En primer lugar, no necesita un prefijo de rep para eso, ya que con una latencia de inicio de ~ 9 ciclos reclamada, es casi seguro que esté mejor con un simple movimiento DWORD o QWORD con un poco de giro para enmascarar lo no utilizado bytes (o tal vez con 2 bytes explícitos, la palabra se mov si sabe que el tamaño es exactamente tres).

Continúan diciendo:

Las instrucciones de MOVER / ALMACENAR cadenas tienen múltiples granularidades de datos. Para un movimiento de datos eficiente, son preferibles las granularidades de datos más grandes. Esto significa que se puede lograr una mayor eficiencia al descomponer un valor de contador arbitrario en varias 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 combinación 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 arquitectura (y pretenden abarcar arquitecturas de casi dos décadas incluso en el manual actual), y las secciones antiguas a menudo no se actualizan para reemplazar o hacer recomendaciones condicionales. eso no se aplica a la arquitectura actual.

Luego cubren ERMSB explícitamente en la sección 3.7.6.

No repasaré los consejos restantes exhaustivamente, pero resumiré las partes buenas en el "por qué usarlo" a continuación.

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

Consideraciones tecnicas

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 rep movs

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

    • Evitar la solicitud de 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 de tipo memcpy , pero aún requiere un par de lecturas para memcpy " 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 ordenar entre las tiendas dentro de 3 un solo rep movs que puede ayudar a simplificar el tráfico de coherencia y simplemente otros aspectos del movimiento de bloque, frente a instrucciones simples de movimiento que deben obedecer un orden de memoria bastante estricto 4 .

  3. En principio, la instrucción rep movs podría aprovechar varios trucos arquitectónicos que no están expuestos en el ISA. Por ejemplo, las arquitecturas pueden tener rutas de datos internas más amplias que el ISA expone 5 y los 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 las regiones superpuestas, por lo que puede ignorar esa posibilidad, pero rep movsb permite y debe producir el resultado esperado. En las implementaciones actuales afecta principalmente a la sobrecarga de inicio, pero probablemente no al rendimiento de bloques grandes. Del mismo modo, rep movsb debe admitir copias granulares de bytes, incluso si realmente lo está utilizando para copiar bloques grandes que son múltiplos de una gran potencia de 2.

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

Resultados de la prueba

Aquí están los resultados de las pruebas para muchos métodos de copia diferentes de tinymembnech en mi i7-6700HQ a 2.6 GHz (lástima que tengo la CPU idéntica, por lo que no estamos obteniendo 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

Algunas conclusiones 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ápido que los enfoques "C" que copian 8 bytes a la vez.
  • Los métodos "no temporales" son más rápidos, hasta aproximadamente un 26% que los que se 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 almacenes no temporales, usar copias de 8 bytes de C es casi tan bueno como la carga / almacenes SSE de 128 bits de ancho. Esto se debe a que un buen bucle 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 importa debido a la nota anterior.
  • El aumento del rendimiento de los enfoques de almacenamiento no temporal sobre los temporales es de aproximadamente 1.45x, que está muy cerca del 1.5x que esperaría si NT elimina 1 de 3 transferencias (es decir, 1 lectura, 1 escritura para NT vs 2 lecturas, 1 escritura). 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 puede saturar su ancho de banda de memoria de un solo hilo, lo que cambia drásticamente el comportamiento.
  • rep movsd parece usar la misma magia que rep movsb en este chip. Eso es interesante porque ERMSB solo apunta explícitamente a movsb y las pruebas anteriores en arcos anteriores con ERMSB muestran que movsb funciona 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 extraen 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 la memcpy no temporal, pero solo en aproximadamente un 14% aquí (en comparación con ~ 26% en la prueba de Skylake). La ventaja de las técnicas de NT sobre 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 pueda saber exactamente, como desarrollos futuros.

Una nota de que la alternativa a rep movsb puede ser la memcpy optimizada de libc (incluidas las copias integradas por el compilador), 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, la "simplicidad" ayuda contra una versión enrollada a mano, pero no contra la memcpy ), pero algunas se aplican a ambas.

Restricciones a las instrucciones disponibles.

En algunos entornos hay una restricción en ciertas instrucciones o el uso de ciertos registros. Por ejemplo, en el kernel de Linux, generalmente no se permite el uso de registros SSE / AVX o FP. Por lo tanto, la mayoría de las variantes de memcpy optimizadas no se pueden usar ya que dependen de registros SSE o AVX, y se usa una copia simple basada en mov 64 bits en x86. 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 usa despacho específico de hardware (por ejemplo, usando cpuid ). Aquí es posible que se vea obligado a usar solo conjuntos de instrucciones más antiguos, lo que descarta cualquier AVX, etc. rep movsb podría ser un buen enfoque, ya que permite el acceso "oculto" a cargas y tiendas más amplias sin usar nuevas instrucciones. Sin embargo, si apuntas al hardware anterior a ERMSB, tendrías que ver si el rendimiento de rep movsb es aceptable allí ...

Pruebas de futuro

Un buen aspecto de rep movsb es que, en teoría , puede aprovechar la mejora arquitectónica en arquitecturas futuras, sin cambios en la fuente, que los movimientos explícitos no pueden. Por ejemplo, cuando se introdujeron rutas de datos de 256 bits, el rep movsb pudo aprovecharlas (como afirmó Intel) sin ningún cambio necesario en el software. El software que utiliza movimientos de 128 bits (que era óptimo antes de Haswell) tendría que modificarse y compilarse.

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 determinar qué tan rápido es probable que sean estas instrucciones en el futuro. Sin embargo, al menos Intel es un tipo de usos orientadores en esta dirección, al comprometerse con un rendimiento al menos razonable en el futuro ( 15.3.3.6 ):

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

Superposición con trabajos posteriores

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

Intel señala este beneficio 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 no consumidor. La heurística se puede entender usando 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 que no consumen puedan llegar a la jubilación.

• Una instancia de REP STOSB mejorado con ECX = 4096 se decodifica como un flujo microoperativo largo 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 desacopla del retiro de la orden del programa, una parte sustancial del flujo de código no consumidor puede procesar a través de la emisión / ejecución y retiro, esencialmente sin costo si la secuencia no consumidora no compite para almacenar recursos de búfer.

Entonces Intel dice que después de todos algunos uops, el código después de que el rep movsb haya emitido, pero aunque muchas tiendas todavía están en vuelo y el rep movsb en su conjunto aún no se ha retirado, los uops de seguir las instrucciones pueden avanzar más a través de la salida -de orden de la maquinaria que podrían si ese código viniera después de un bucle de copia.

Los uops de un ciclo de carga y almacenamiento explícito tienen que retirarse por separado en el orden del programa. Eso tiene que suceder para dejar espacio en el ROB para seguir a Uops.

No parece haber mucha información detallada sobre cuánto tiempo rep movsb exactamente la instrucción microcodificada como rep movsb . No sabemos exactamente cómo las ramas de microcódigo solicitan una secuencia diferente de uops del secuenciador de microcódigo, o cómo se retiran los uops. Si los uops individuales no tienen que retirarse por separado, ¿tal vez toda la instrucción solo ocupa un espacio en el ROB?

Cuando el front-end que alimenta la maquinaria OoO ve una instrucción rep movsb en la caché de uop, activa la ROM del secuenciador de microcódigo (MS-ROM) para enviar microcódigos uops a la cola que alimenta la etapa de emisión / cambio de nombre. Probablemente no sea posible que ningún otro uops se mezcle con eso y emita / ejecute 8 mientras se sigue emitiendo rep movsb , pero las instrucciones posteriores se pueden recuperar / decodificar y emitir justo después del último rep movsb uop, mientras que parte de la copia hasn No se ha ejecutado todavía. Esto solo es útil si al menos parte de su código posterior no depende del resultado de la memcpy (que 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 instrucción lenta rep movsb , en cuyo punto se detendrá, donde N es el tamaño ROB . Con tamaños actuales de ROB de ~ 200 (192 en Haswell, 224 en Skylake), ese es un beneficio máximo de ~ 200 ciclos de trabajo libre para el código posterior con un IPC de 1. En 200 ciclos, puede copiar en algún lugar alrededor de 800 bytes a 10 GB / s, por lo que para las copias de ese tamaño puede obtener un trabajo gratuito cercano al costo de la copia (de alguna manera, haciendo que la copia sea gratuita).

Sin embargo, a medida que el tamaño de las copias es mucho mayor, 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.

Copiar bucles tampoco bloquea totalmente la ejecución de instrucciones posteriores. Intel no entra en detalles sobre el tamaño del beneficio, o sobre qué tipo de copias o código circundante es el mayor beneficio. (Fuente o destino frío o caliente, ILP alto o código de alta latencia ILP bajo después).

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 se ve limitado por las fallas de i-cache (incluyendo uop cache), el tamaño reducido del código podría ser beneficioso.

Nuevamente, podemos limitar la magnitud de este beneficio en función del tamaño de la copia. Realmente no lo resolveré numéricamente, pero la intuición es que reducir el tamaño del código dinámico en B bytes puede ahorrar como máximo C * B memcpy caché, por algo de C. constante Cada llamada a memcpy incurre en el costo de pérdida de caché (o beneficio) una vez, pero la ventaja de un mayor rendimiento se escala con el número de bytes copiados. Entonces, para transferencias grandes, un mayor rendimiento dominará los efectos de caché.

Nuevamente, esto no es algo que se mostrará en un punto de referencia simple, donde el bucle completo sin duda cabe en la caché de uop. Necesitará una prueba real en el lugar para evaluar este efecto.

Optimización específica de arquitectura

Usted informó que en su hardware, rep movsb era considerablemente más lento que la memcpy de la plataforma. 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 todas las generaciones, por lo que puede ser más rápido o al menos vinculado (en ese momento puede ganar en función de otras ventajas) en las arquitecturas donde ha estado actualizado, solo para quedarse atrás en el hardware posterior.

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

La gran debilidad de hacer secuencias rápidas en el microcódigo era que el [...] microcódigo se desintonizaba con cada generación, cada vez más lento hasta que alguien lo arreglaba. Al igual que una copia de la biblioteca, los hombres se desafinan. Supongo que es posible que una de las oportunidades perdidas sea utilizar cargas y tiendas de 128 bits cuando estén disponibles, y así sucesivamente.

En ese caso, puede verse como otra optimización "específica de la plataforma" para aplicar en las rutinas de memcpy típicas de cada truco en el libro que se encuentran en bibliotecas estándar y compiladores JIT: pero solo para usar en arquitecturas donde está mejor. Para cosas compiladas JIT o AOT esto es fácil, pero para binarios compilados estáticamente esto requiere un despacho específico de la plataforma, pero eso a menudo ya existe (a veces implementado en el momento del enlace), o el argumento mtune se puede usar para tomar una decisión estática.

Sencillez

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

Plataformas limitadas de latencia

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

El primer modo es 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, lo mismo que se informa en ARK .

No obtendrá más que esa tasa de DRAM (y generalmente algo menos debido a diversas ineficiencias) agregadas en todos los núcleos del zócalo (es decir, es un límite global para sistemas de un solo zócalo).

El otro límite se impone por la cantidad de solicitudes simultáneas que un núcleo puede emitir realmente al subsistema de memoria. Imagine si un núcleo solo pudiera tener 1 solicitud en progreso a la vez, para una línea de caché de 64 bytes; cuando se complete la solicitud, podría emitir otra. Supongamos también una latencia de memoria muy rápida de 50 ns. Luego, 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 buffers de relleno de línea por núcleo entre L1 y el resto de la jerarquía de memoria, y tal vez 16 o más buffers de relleno entre L2 y DRAM. La captación previa compite por los mismos recursos, pero al menos ayuda a reducir la latencia efectiva. Para obtener más detalles, consulte cualquiera de las excelentes publicaciones que el Dr. Bandwidth ha escrito 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 de 12 a 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 "cliente" de gen 2 canales recientes, que parecen tener un mejor puntaje, quizás más buffers de línea pueden alcanzar el límite 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 un ancho de banda de DRAM de 50 GB / s, mientras que solo debe mantener <20 GB / s por núcleo debido a los límites de concurrencia: el límite anterior es para todo el socket y el segundo es por núcleo. Por lo tanto, cada núcleo en un sistema de 8 núcleos puede enviar solicitudes por valor de 20 GB / s, momento en el que volverán a tener una DRAM limitada.

¿Por qué sigo y sigo 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 están nuestros chips, pero la mayoría no está en un solo núcleo), el uso de escrituras no temporales se vuelve muy importante ya que ahorra la lectura de propiedad que normalmente desperdicia 1/3 de su ancho de banda. Puede ver eso exactamente en los resultados de las pruebas anteriores: las implementaciones de memcpy que no usan tiendas NT pierden 1/3 de su ancho de banda.

Sin embargo, si su concurrencia es limitada, la situación se iguala y, a veces, se invierte. Tiene ancho de banda DRAM de sobra, por lo que las tiendas NT no ayudan e incluso pueden dañar, ya que pueden aumentar la latencia ya que el tiempo de transferencia para el búfer de línea puede ser más largo que un escenario donde la captación previa lleva la línea RFO a LLC (o incluso L2) y luego la tienda se completa en LLC para una latencia efectiva más baja. Finalmente, la falta de núcleo del servidor tiende a tener almacenes NT mucho más lentos que los clientes (y gran ancho de banda), lo que acentúa este efecto.

Entonces, en otras plataformas, puede encontrar que las tiendas NT son menos útiles (al menos cuando le importa el rendimiento de un solo subproceso) y tal vez el rep movsb gana donde (si obtiene lo mejor de ambos mundos).

Realmente, este último elemento es una llamada para la mayoría de las pruebas. Sé que las tiendas NT pierden su aparente ventaja para las pruebas de subproceso único en la mayoría de los archs (incluidos los archs del servidor actual), pero no sé cómo el rep movsb se desempeñará relativamente ...

Referencias

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

investigación comp.arch de rep movsb versus alternativas. Muchas buenas notas sobre la predicción de bifurcaciones y una implementación del enfoque que a menudo he sugerido para bloques pequeños: superponer la primera y / o la última lectura / escritura en lugar de intentar 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 casos donde, 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 clave tener en cuenta que esto solo se aplica a las diversas tiendas dentro de la instrucción individual: una vez completada, el bloque de tiendas aún aparece ordenado con respecto a las tiendas anteriores y posteriores. Por lo tanto, el código puede ver las tiendas del 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 las tiendas discretas no temporales también evitan la mayoría de los requisitos de pedido, aunque en la práctica rep movs tiene aún más libertad ya que todavía hay algunas restricciones de pedido en las tiendas 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 en día, los chips "castrados", como las marcas Pentium o Celeron, tienen AVX desactivado, pero presumiblemente el microcódigo rep movs todavía puede usar 256b de cargas / tiendas.

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

7 Estoy asumiendo que "estándar" memcpy está eligiendo un enfoque no temporal, que es muy probable para este tamaño de búfer.

8 Eso no es necesariamente obvio, ya que podría ser el caso de que la secuencia uop generada por el rep movsb despacho simplemente monopolice y luego se parecería mucho al mov caso explícito . Sin embargo, parece que no funciona así: uops de instrucciones posteriores pueden mezclarse con uops del microcodificado rep movsb .

9 Es decir, aquellos que pueden emitir una gran cantidad de solicitudes de memoria independientes y, por lo tanto, saturar el ancho de banda DRAM-to-core disponible, de los cuales memcpy sería un póster secundario (y según lo dispuesto para cargas limitadas de latencia pura como la búsqueda de puntero).