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
- 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.
- 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 *.
- 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.
- 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
-
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 tipomemcpy
, pero aún requiere un par de lecturas paramemcpy
" 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 puederep movsb
exactamente.
-
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 . -
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 losrep movs
podrían usar internamente.
Desventajas
-
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, perorep 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. -
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 losrep 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 querep movsb
en este chip. Eso es interesante porque ERMSB solo apunta explícitamente amovsb
y las pruebas anteriores en arcos anteriores con ERMSB muestran quemovsb
funciona mucho más rápido quemovsd
. Esto es principalmente académico ya quemovsb
es más general quemovsd
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).