telefono - porque mi celular dice memoria llena si no tengo nada
¿Por qué escribir en la memoria es mucho más lento que leerlo? (7)
Aquí hay una referencia de ancho de banda memset
simple:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int main()
{
unsigned long n, r, i;
unsigned char *p;
clock_t c0, c1;
double elapsed;
n = 1000 * 1000 * 1000; /* GB */
r = 100; /* repeat */
p = calloc(n, 1);
c0 = clock();
for(i = 0; i < r; ++i) {
memset(p, (int)i, n);
printf("%4d/%4ld/r", p[0], r); /* "use" the result */
fflush(stdout);
}
c1 = clock();
elapsed = (c1 - c0) / (double)CLOCKS_PER_SEC;
printf("Bandwidth = %6.3f GB/s (Giga = 10^9)/n", (double)n * r / elapsed / 1e9);
free(p);
}
En mi sistema (detalles a continuación) con un solo módulo de memoria DDR3-1600, genera:
Ancho de banda = 4.751 GB / s (Giga = 10 ^ 9)
Esto es el 37% de la velocidad teórica de RAM: 1.6 GHz * 8 bytes = 12.8 GB/s
Por otro lado, aquí hay una prueba similar de "lectura":
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
unsigned long do_xor(const unsigned long* p, unsigned long n)
{
unsigned long i, x = 0;
for(i = 0; i < n; ++i)
x ^= p[i];
return x;
}
int main()
{
unsigned long n, r, i;
unsigned long *p;
clock_t c0, c1;
double elapsed;
n = 1000 * 1000 * 1000; /* GB */
r = 100; /* repeat */
p = calloc(n/sizeof(unsigned long), sizeof(unsigned long));
c0 = clock();
for(i = 0; i < r; ++i) {
p[0] = do_xor(p, n / sizeof(unsigned long)); /* "use" the result */
printf("%4ld/%4ld/r", i, r);
fflush(stdout);
}
c1 = clock();
elapsed = (c1 - c0) / (double)CLOCKS_PER_SEC;
printf("Bandwidth = %6.3f GB/s (Giga = 10^9)/n", (double)n * r / elapsed / 1e9);
free(p);
}
Emite:
Ancho de banda = 11.516 GB / s (Giga = 10 ^ 9)
Puedo acercarme al límite teórico para el rendimiento de lectura, como XORing una matriz grande, pero la escritura parece ser mucho más lenta. ¿Por qué?
SO Ubuntu 14.04 AMD64 (compilo con gcc -O3
. Usar -O3 -march=native
hace que el rendimiento de lectura sea ligeramente peor, pero no afecta a memset
)
CPU Xeon E5-2630 v2
RAM Un único "16GB PC3-12800 Parity REG CL11 240-Pin DIMM" (Lo que dice en la caja) Creo que tener un solo DIMM hace que el rendimiento sea más predecible. Supongo que con 4 DIMM, memset
será hasta 4 veces más rápido.
Placa base Supermicro X9DRG-QF (Admite memoria de 4 canales)
Sistema adicional : una computadora portátil con 2x 4 GB de memoria RAM DDR3-1067: la lectura y la escritura son de aproximadamente 5,5 GB / s, pero tenga en cuenta que utiliza 2 módulos DIMM.
PS reemplazar memset
con esta versión da como resultado exactamente el mismo rendimiento
void *my_memset(void *s, int c, size_t n)
{
unsigned long i = 0;
for(i = 0; i < n; ++i)
((char*)s)[i] = (char)c;
return s;
}
Aquí está mi hipótesis de trabajo. Si es correcto, explica por qué las escrituras son aproximadamente dos veces más lentas que las lecturas:
Aunque memset
solo escribe en la memoria virtual, haciendo caso omiso de sus contenidos anteriores, a nivel de hardware, la computadora no puede hacer una escritura pura en DRAM: lee los contenidos de DRAM en caché, los modifica allí y luego los escribe de nuevo en DRAM. Por lo tanto, a nivel de hardware, memset
lee y escribe (¡aunque el primero parece inútil)! De ahí la diferencia de velocidad aproximadamente dos veces mayor.
Con tus programas, obtengo
(write) Bandwidth = 6.076 GB/s
(read) Bandwidth = 10.916 GB/s
en una máquina de escritorio (Core i7, x86-64, GCC 4.9, GNU libc 2.19) con seis DIMM de 2 GB. (No tengo más detalles que eso a mano, lo siento).
Sin embargo, este programa informa ancho de banda de 12.209 GB/s
de 12.209 GB/s
:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <emmintrin.h>
static void
nt_memset(char *buf, unsigned char val, size_t n)
{
/* this will only work with aligned address and size */
assert((uintptr_t)buf % sizeof(__m128i) == 0);
assert(n % sizeof(__m128i) == 0);
__m128i xval = _mm_set_epi8(val, val, val, val,
val, val, val, val,
val, val, val, val,
val, val, val, val);
for (__m128i *p = (__m128i*)buf; p < (__m128i*)(buf + n); p++)
_mm_stream_si128(p, xval);
_mm_sfence();
}
/* same main() as your write test, except calling nt_memset instead of memset */
La magia está en _mm_stream_si128
, también conocida como la instrucción de la máquina movntdq
, que escribe una cantidad de 16 bytes en la memoria RAM del sistema, evitando el caché (la jerga oficial para esto es " almacenamiento no temporal "). Creo que esto demuestra de manera bastante concluyente que la diferencia en el rendimiento tiene que ver con el comportamiento del caché.
NB glibc 2.19 tiene un memset
elaboradamente optimizado a memset
que hace uso de instrucciones vectoriales. Sin embargo, no usa tiendas no temporales. Eso es probablemente lo correcto para memset
; en general, borra la memoria poco antes de usarla, por lo que desea que esté caliente en la memoria caché. (Supongo que un memset
aún más inteligente podría cambiar a tiendas no temporales por bloques realmente grandes , con la teoría de que no podrías querer todo eso en el caché, porque el caché simplemente no es tan grande).
Dump of assembler code for function memset:
=> 0x00007ffff7ab9420 <+0>: movd %esi,%xmm8
0x00007ffff7ab9425 <+5>: mov %rdi,%rax
0x00007ffff7ab9428 <+8>: punpcklbw %xmm8,%xmm8
0x00007ffff7ab942d <+13>: punpcklwd %xmm8,%xmm8
0x00007ffff7ab9432 <+18>: pshufd $0x0,%xmm8,%xmm8
0x00007ffff7ab9438 <+24>: cmp $0x40,%rdx
0x00007ffff7ab943c <+28>: ja 0x7ffff7ab9470 <memset+80>
0x00007ffff7ab943e <+30>: cmp $0x10,%rdx
0x00007ffff7ab9442 <+34>: jbe 0x7ffff7ab94e2 <memset+194>
0x00007ffff7ab9448 <+40>: cmp $0x20,%rdx
0x00007ffff7ab944c <+44>: movdqu %xmm8,(%rdi)
0x00007ffff7ab9451 <+49>: movdqu %xmm8,-0x10(%rdi,%rdx,1)
0x00007ffff7ab9458 <+56>: ja 0x7ffff7ab9460 <memset+64>
0x00007ffff7ab945a <+58>: repz retq
0x00007ffff7ab945c <+60>: nopl 0x0(%rax)
0x00007ffff7ab9460 <+64>: movdqu %xmm8,0x10(%rdi)
0x00007ffff7ab9466 <+70>: movdqu %xmm8,-0x20(%rdi,%rdx,1)
0x00007ffff7ab946d <+77>: retq
0x00007ffff7ab946e <+78>: xchg %ax,%ax
0x00007ffff7ab9470 <+80>: lea 0x40(%rdi),%rcx
0x00007ffff7ab9474 <+84>: movdqu %xmm8,(%rdi)
0x00007ffff7ab9479 <+89>: and $0xffffffffffffffc0,%rcx
0x00007ffff7ab947d <+93>: movdqu %xmm8,-0x10(%rdi,%rdx,1)
0x00007ffff7ab9484 <+100>: movdqu %xmm8,0x10(%rdi)
0x00007ffff7ab948a <+106>: movdqu %xmm8,-0x20(%rdi,%rdx,1)
0x00007ffff7ab9491 <+113>: movdqu %xmm8,0x20(%rdi)
0x00007ffff7ab9497 <+119>: movdqu %xmm8,-0x30(%rdi,%rdx,1)
0x00007ffff7ab949e <+126>: movdqu %xmm8,0x30(%rdi)
0x00007ffff7ab94a4 <+132>: movdqu %xmm8,-0x40(%rdi,%rdx,1)
0x00007ffff7ab94ab <+139>: add %rdi,%rdx
0x00007ffff7ab94ae <+142>: and $0xffffffffffffffc0,%rdx
0x00007ffff7ab94b2 <+146>: cmp %rdx,%rcx
0x00007ffff7ab94b5 <+149>: je 0x7ffff7ab945a <memset+58>
0x00007ffff7ab94b7 <+151>: nopw 0x0(%rax,%rax,1)
0x00007ffff7ab94c0 <+160>: movdqa %xmm8,(%rcx)
0x00007ffff7ab94c5 <+165>: movdqa %xmm8,0x10(%rcx)
0x00007ffff7ab94cb <+171>: movdqa %xmm8,0x20(%rcx)
0x00007ffff7ab94d1 <+177>: movdqa %xmm8,0x30(%rcx)
0x00007ffff7ab94d7 <+183>: add $0x40,%rcx
0x00007ffff7ab94db <+187>: cmp %rcx,%rdx
0x00007ffff7ab94de <+190>: jne 0x7ffff7ab94c0 <memset+160>
0x00007ffff7ab94e0 <+192>: repz retq
0x00007ffff7ab94e2 <+194>: movq %xmm8,%rcx
0x00007ffff7ab94e7 <+199>: test $0x18,%dl
0x00007ffff7ab94ea <+202>: jne 0x7ffff7ab950e <memset+238>
0x00007ffff7ab94ec <+204>: test $0x4,%dl
0x00007ffff7ab94ef <+207>: jne 0x7ffff7ab9507 <memset+231>
0x00007ffff7ab94f1 <+209>: test $0x1,%dl
0x00007ffff7ab94f4 <+212>: je 0x7ffff7ab94f8 <memset+216>
0x00007ffff7ab94f6 <+214>: mov %cl,(%rdi)
0x00007ffff7ab94f8 <+216>: test $0x2,%dl
0x00007ffff7ab94fb <+219>: je 0x7ffff7ab945a <memset+58>
0x00007ffff7ab9501 <+225>: mov %cx,-0x2(%rax,%rdx,1)
0x00007ffff7ab9506 <+230>: retq
0x00007ffff7ab9507 <+231>: mov %ecx,(%rdi)
0x00007ffff7ab9509 <+233>: mov %ecx,-0x4(%rdi,%rdx,1)
0x00007ffff7ab950d <+237>: retq
0x00007ffff7ab950e <+238>: mov %rcx,(%rdi)
0x00007ffff7ab9511 <+241>: mov %rcx,-0x8(%rdi,%rdx,1)
0x00007ffff7ab9516 <+246>: retq
(Esto está en libc.so.6
, no en el programa en sí; la otra persona que intentó volcar el ensamble para memset
parece haber encontrado su entrada PLT. La forma más fácil de obtener el volcado de ensamblaje para el memset
real en un El sistema Unixy es
$ gdb ./a.out
(gdb) set env LD_BIND_NOW t
(gdb) b main
Breakpoint 1 at [address]
(gdb) r
Breakpoint 1, [address] in main ()
(gdb) disas memset
...
.)
El almacenamiento en caché y la localidad casi con seguridad explican la mayoría de los efectos que está viendo.
No hay ningún caché o localidad en las escrituras, a menos que desee un sistema no determinista. La mayoría de los tiempos de escritura se miden como el tiempo que tardan los datos en llegar al medio de almacenamiento (ya sea un disco duro o un chip de memoria), mientras que las lecturas pueden provenir de cualquier cantidad de capas de caché que sean más rápidas que el medio de almacenamiento.
La diferencia, al menos en mi máquina, con un procesador AMD, es que el programa de lectura utiliza operaciones vectorizadas. Descompilar los dos rinde esto para el programa de escritura:
0000000000400610 <main>:
...
400628: e8 73 ff ff ff callq 4005a0 <clock@plt>
40062d: 49 89 c4 mov %rax,%r12
400630: 89 de mov %ebx,%esi
400632: ba 00 ca 9a 3b mov $0x3b9aca00,%edx
400637: 48 89 ef mov %rbp,%rdi
40063a: e8 71 ff ff ff callq 4005b0 <memset@plt>
40063f: 0f b6 55 00 movzbl 0x0(%rbp),%edx
400643: b9 64 00 00 00 mov $0x64,%ecx
400648: be 34 08 40 00 mov $0x400834,%esi
40064d: bf 01 00 00 00 mov $0x1,%edi
400652: 31 c0 xor %eax,%eax
400654: 48 83 c3 01 add $0x1,%rbx
400658: e8 a3 ff ff ff callq 400600 <__printf_chk@plt>
Pero esto para el programa de lectura:
00000000004005d0 <main>:
....
400609: e8 62 ff ff ff callq 400570 <clock@plt>
40060e: 49 d1 ee shr %r14
400611: 48 89 44 24 18 mov %rax,0x18(%rsp)
400616: 4b 8d 04 e7 lea (%r15,%r12,8),%rax
40061a: 4b 8d 1c 36 lea (%r14,%r14,1),%rbx
40061e: 48 89 44 24 10 mov %rax,0x10(%rsp)
400623: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
400628: 4d 85 e4 test %r12,%r12
40062b: 0f 84 df 00 00 00 je 400710 <main+0x140>
400631: 49 8b 17 mov (%r15),%rdx
400634: bf 01 00 00 00 mov $0x1,%edi
400639: 48 8b 74 24 10 mov 0x10(%rsp),%rsi
40063e: 66 0f ef c0 pxor %xmm0,%xmm0
400642: 31 c9 xor %ecx,%ecx
400644: 0f 1f 40 00 nopl 0x0(%rax)
400648: 48 83 c1 01 add $0x1,%rcx
40064c: 66 0f ef 06 pxor (%rsi),%xmm0
400650: 48 83 c6 10 add $0x10,%rsi
400654: 49 39 ce cmp %rcx,%r14
400657: 77 ef ja 400648 <main+0x78>
400659: 66 0f 6f d0 movdqa %xmm0,%xmm2 ;!!!! vectorized magic
40065d: 48 01 df add %rbx,%rdi
400660: 66 0f 73 da 08 psrldq $0x8,%xmm2
400665: 66 0f ef c2 pxor %xmm2,%xmm0
400669: 66 0f 7f 04 24 movdqa %xmm0,(%rsp)
40066e: 48 8b 04 24 mov (%rsp),%rax
400672: 48 31 d0 xor %rdx,%rax
400675: 48 39 dd cmp %rbx,%rbp
400678: 74 04 je 40067e <main+0xae>
40067a: 49 33 04 ff xor (%r15,%rdi,8),%rax
40067e: 4c 89 ea mov %r13,%rdx
400681: 49 89 07 mov %rax,(%r15)
400684: b9 64 00 00 00 mov $0x64,%ecx
400689: be 04 0a 40 00 mov $0x400a04,%esi
400695: e8 26 ff ff ff callq 4005c0 <__printf_chk@plt>
40068e: bf 01 00 00 00 mov $0x1,%edi
400693: 31 c0 xor %eax,%eax
Además, tenga en cuenta que su memset
"de cosecha propia" en realidad está optimizado hasta una llamada a memset
:
00000000004007b0 <my_memset>:
4007b0: 48 85 d2 test %rdx,%rdx
4007b3: 74 1b je 4007d0 <my_memset+0x20>
4007b5: 48 83 ec 08 sub $0x8,%rsp
4007b9: 40 0f be f6 movsbl %sil,%esi
4007bd: e8 ee fd ff ff callq 4005b0 <memset@plt>
4007c2: 48 83 c4 08 add $0x8,%rsp
4007c6: c3 retq
4007c7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
4007ce: 00 00
4007d0: 48 89 f8 mov %rdi,%rax
4007d3: c3 retq
4007d4: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4007db: 00 00 00
4007de: 66 90 xchg %ax,%ax
No puedo encontrar ninguna referencia con respecto a si memset
usa o no operaciones vectorizadas, el desmontaje de memset@plt
es útil aquí:
00000000004005b0 <memset@plt>:
4005b0: ff 25 72 0a 20 00 jmpq *0x200a72(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
4005b6: 68 02 00 00 00 pushq $0x2
4005bb: e9 c0 ff ff ff jmpq 400580 <_init+0x20>
Esta pregunta sugiere que, dado que memset
está diseñado para manejar todos los casos, es posible que falten algunas optimizaciones.
Este chico definitivamente parece convencido de que necesitas enrollar tu propio memset
ensamblador para aprovechar las instrucciones SIMD. Esta pregunta también lo hace .
Voy a tomar una foto en la oscuridad y supongo que no está usando operaciones SIMD porque no puede decir si va a operar en algo que es un múltiplo del tamaño de una operación vectorizada, o si hay algo de alineación -asunto relacionado.
Sin embargo, podemos confirmar que no es un problema de eficiencia de caché al verificar con cachegrind
. El programa de escritura produce lo siguiente:
==19593== D refs: 6,312,618,768 (80,386 rd + 6,312,538,382 wr)
==19593== D1 misses: 1,578,132,439 ( 5,350 rd + 1,578,127,089 wr)
==19593== LLd misses: 1,578,131,849 ( 4,806 rd + 1,578,127,043 wr)
==19593== D1 miss rate: 24.9% ( 6.6% + 24.9% )
==19593== LLd miss rate: 24.9% ( 5.9% + 24.9% )
==19593==
==19593== LL refs: 1,578,133,467 ( 6,378 rd + 1,578,127,089 wr)
==19593== LL misses: 1,578,132,871 ( 5,828 rd + 1,578,127,043 wr) <<
==19593== LL miss rate: 9.0% ( 0.0% + 24.9% )
y el programa de lectura produce:
==19682== D refs: 6,312,618,618 (6,250,080,336 rd + 62,538,282 wr)
==19682== D1 misses: 1,578,132,331 (1,562,505,046 rd + 15,627,285 wr)
==19682== LLd misses: 1,578,131,740 (1,562,504,500 rd + 15,627,240 wr)
==19682== D1 miss rate: 24.9% ( 24.9% + 24.9% )
==19682== LLd miss rate: 24.9% ( 24.9% + 24.9% )
==19682==
==19682== LL refs: 1,578,133,357 (1,562,506,072 rd + 15,627,285 wr)
==19682== LL misses: 1,578,132,760 (1,562,505,520 rd + 15,627,240 wr) <<
==19682== LL miss rate: 4.1% ( 4.1% + 24.9% )
Mientras que el programa de lectura tiene una tasa de errores LL menor porque realiza muchas lecturas más (una lectura extra por operación XOR
), el número total de errores es el mismo. Entonces, sea cual sea el problema, no está allí.
La principal diferencia en el rendimiento proviene de la política de almacenamiento en caché de su PC / región de memoria. Cuando lee desde una memoria y los datos no están en la memoria caché, la memoria debe primero buscarse en la memoria caché a través del bus de memoria antes de poder realizar cualquier cálculo con los datos. Sin embargo, cuando escribe en la memoria hay diferentes políticas de escritura. Lo más probable es que su sistema use caché de escritura no simultánea (o más precisamente "asignación de escritura"), lo que significa que cuando escribe en una ubicación de memoria que no está en la caché, los datos primero se extraen de la memoria y luego se escriben volver a la memoria cuando se desalojan los datos de la memoria caché, lo que significa el viaje de ida y vuelta para los datos y el uso de ancho de banda del bus 2 veces al escribir. También hay una política de caché de escritura directa (o "asignación de no escritura") que generalmente significa que al cache-miss en las escrituras los datos no se captan en la memoria caché, y que deberían acercarse al mismo rendimiento para ambas lecturas y escribe
Podría ser exactamente cómo se lleva a cabo (el-sistema-como-un-todo). La lectura más rápida parece ser una tendencia común con una amplia gama de rendimiento de rendimiento relativo. En un análisis rápido de los gráficos DDR3 Intel y DDR2 listados, como algunos casos seleccionados de (escritura / lectura)% ;
Algunos de los mejores chips DDR3 están escribiendo a aproximadamente ~ 60-70% del rendimiento de lectura. Sin embargo, hay algunos módulos de memoria (es decir, Golden Empire CL11-13-13 D3-2666) hasta solo ~ 30% de escritura.
Los chips DDR2 de mejor rendimiento parecen tener solo aproximadamente ~ 50% del rendimiento de escritura en comparación con la lectura. Pero también hay algunos contendientes notablemente malos (es decir, OCZ OCZ21066NEW_BT1G) hasta ~ 20%.
Si bien esto puede no explicar la causa del ~ 40% de escritura / lectura informado, como el código de referencia y la configuración utilizada es probablemente diferente (las notas son vagas ), este es definitivamente un factor. (Diría algunos programas de referencia existentes y veo si los números coinciden con los del código publicado en la pregunta).
Actualizar:
Descargué la tabla de búsqueda de memoria desde el sitio vinculado y la procesé en Excel. Si bien todavía muestra una amplia gama de valores, es mucho menos grave que la respuesta original anterior, que solo analizó los chips de memoria de lectura superior y algunas entradas "interesantes" seleccionadas de los gráficos. No estoy seguro de por qué las discrepancias, especialmente en los terribles contendientes señalados anteriormente, no están presentes en la lista secundaria.
Sin embargo, incluso con los nuevos números, la diferencia todavía varía ampliamente del 50% al 100% (mediana 65, media 65) del rendimiento de lectura. Tenga en cuenta que el hecho de que un chip sea "100%" eficiente en una relación de escritura / lectura no significa que sea mejor en general ... solo que fue más equilibrado entre las dos operaciones.
Porque para leer, simplemente pulsa las líneas de dirección y lee los estados centrales en las líneas de detección. El ciclo de recuperación se produce después de que los datos se entregan a la CPU y, por lo tanto, no disminuyen la velocidad. Por otro lado, para escribir primero debe realizar una lectura falsa para restablecer los núcleos, luego realice el ciclo de escritura.
(En caso de que no sea obvio, esta respuesta es irónica: describe por qué escribir es más lento que leer en una vieja caja de memoria central).