Voici un simple repère de bande passante memset
:
#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);
}
Sur mon système (détails ci-dessous) avec un seul module de mémoire DDR3-1600, il sort:
Bande passante = 4,751 Go/s (Giga = 10 ^ 9)
Cela représente 37% de la vitesse théorique RAM speed: 1.6 GHz * 8 bytes = 12.8 GB/s
D'un autre côté, voici un test de "lecture" similaire:
#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);
}
Il génère:
Bande passante = 11,516 Go/s (Giga = 10 ^ 9)
Je peux me rapprocher de la limite théorique des performances de lecture, comme XORing un grand tableau, mais l'écriture semble être beaucoup plus lente. Pourquoi?
OS Ubuntu 14.04 AMD64 (je compile avec gcc -O3
. En utilisant -O3 -march=native
aggrave légèrement les performances de lecture, mais n'affecte pas memset
)
CPU Xeon E5-2630 v2
RAM Un seul "DIMM PC3-12800 à 16 broches REG CL11 240 broches" (ce que dit la boîte) Je pense que le fait d'avoir un seul DIMM améliore les performances plus prévisible. Je suppose qu'avec 4 modules DIMM, memset
sera jusqu'à 4 fois plus rapide.
Carte mère Supermicro X9DRG-QF (Prend en charge la mémoire à 4 canaux)
Système supplémentaire : Un ordinateur portable avec 2x 4 Go de RAM DDR3-1067: la lecture et l'écriture sont toutes les deux d'environ 5,5 Go/s, mais notez qu'il utilise 2 modules DIMM.
P.S. remplacer memset
par cette version donne exactement les mêmes performances
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;
}
Avec vos programmes, je reçois
(write) Bandwidth = 6.076 GB/s
(read) Bandwidth = 10.916 GB/s
sur un ordinateur de bureau (Core i7, x86-64, GCC 4.9, GNU libc 2.19) avec six modules DIMM de 2 Go. (Je n'ai pas plus de détails que cela à portée de main, désolé.)
Cependant, ce programme signale une bande passante d'écriture 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 magie est tout en _mm_stream_si128
, alias l'instruction machine movntdq
, qui écrit une quantité de 16 octets dans la RAM système, contournant le cache (le jargon officiel car c'est " mémoire non temporelle "). Je pense que cela démontre de façon assez concluante que la différence de performances est tout sur le comportement du cache.
N.B. la glibc 2.19 possède un memset
minutieusement optimisé à la main qui utilise des instructions vectorielles. Cependant, il n'utilise pas des mémoires non temporelles. C'est probablement la bonne chose pour memset
; en général, vous videz la mémoire peu de temps avant de l'utiliser, donc vous voulez qu'elle soit chaude dans le cache. (Je suppose qu'un memset
encore plus intelligent pourrait basculer vers des magasins non temporels pour bloc vraiment énorme clair, sur la théorie que vous ne pourriez pas voulez peut-être tout cela dans le cache, car le cache n'est tout simplement pas si grand.)
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
(C'est dans libc.so.6
, pas le programme lui-même - l'autre personne qui a tenté de vider l'assembly pour memset
ne semble avoir trouvé que son entrée PLT. Le moyen le plus simple d'obtenir le vidage d'assembly pour le vrai memset
sur un système Unixy est
$ 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
...
.)
La principale différence dans les performances vient de la politique de mise en cache de votre région PC/mémoire. Lorsque vous lisez à partir d'une mémoire et que les données ne sont pas dans le cache, la mémoire doit d'abord être récupérée dans le cache via le bus mémoire avant de pouvoir effectuer un calcul avec les données. Cependant, lorsque vous écrivez dans la mémoire, il existe différentes stratégies d'écriture. Il est fort probable que votre système utilise un cache de réécriture (ou plus précisément "écriture d'allocation"), ce qui signifie que lorsque vous écrivez dans un emplacement de mémoire qui n'est pas dans le cache, les données sont d'abord extraites de la mémoire dans le cache et finalement écrites de retour en mémoire lorsque les données sont supprimées du cache, ce qui signifie un aller-retour pour les données et une utilisation de la bande passante du bus 2x lors des écritures. Il existe également une politique de mise en cache à écriture immédiate (ou "allocation sans écriture"), ce qui signifie généralement qu'en cas de manque de cache lors des écritures, les données ne sont pas récupérées dans le cache, et qui devraient donner des performances plus proches pour les deux lectures et écrit.
La différence - au moins sur ma machine, avec un processeur AMD - est que le programme de lecture utilise des opérations vectorisées. La décompilation des deux produit ceci pour le programme d'écriture:
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>
Mais ceci pour le programme de lecture:
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
Notez également que votre "homegrown" memset
est en fait optimisé jusqu'à un appel à 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
Je ne trouve aucune référence quant à savoir si memset
utilise des opérations vectorisées, le démontage de memset@plt
est inutile ici:
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>
Cette question suggère que puisque memset
est conçu pour gérer tous les cas, il peut manquer des optimisations.
Ce gars semble définitivement convaincu que vous devez rouler votre propre assembleur memset
pour profiter des instructions SIMD. Cette question aussi .
Je vais prendre une photo dans l'obscurité et deviner qu'elle n'utilise pas les opérations SIMD car elle ne peut pas dire si elle va fonctionner sur quelque chose qui est un multiple de la taille d'une opération vectorisée, ou il y a un certain alignement problème lié.
Cependant, nous pouvons confirmer que ce n'est pas un problème d'efficacité du cache en vérifiant avec cachegrind
. Le programme d'écriture produit les éléments suivants:
==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% )
et le programme lu produit:
==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% )
Bien que le programme de lecture ait un taux de LL inférieur car il effectue beaucoup plus de lectures (une lecture supplémentaire par XOR
opération), le nombre total de ratés est le même. Quel que soit le problème, il n'est pas là.
La mise en cache et la localité expliquent presque certainement la plupart des effets que vous voyez.
Il n'y a pas de mise en cache ou de localité sur les écritures, sauf si vous voulez un système non déterministe. La plupart des temps d'écriture sont mesurés comme le temps nécessaire pour que les données parviennent jusqu'au support de stockage (qu'il s'agisse d'un disque dur ou d'une puce de mémoire), tandis que les lectures peuvent provenir de n'importe quel nombre de couches de cache plus rapides que le support de stockage.
Cela pourrait être juste comment il fonctionne (le système dans son ensemble). La lecture étant plus rapide semble être une tendance courante avec une large gamme de performances de débit relatif. Sur une analyse rapide des processeurs DDR3 Intel et DDR2 répertoriés, comme quelques cas sélectionnés de (écriture/lire)% ;
Certaines puces DDR3 les plus performantes écrivent à environ 60 à 70% du débit de lecture. Cependant, il existe certains modules de mémoire (par exemple, Golden Empire CL11-13-13 D3-2666) jusqu'à seulement ~ 30% d'écriture.
Les puces DDR2 les plus performantes semblent n'avoir qu'environ ~ 50% du débit d'écriture par rapport à la lecture. Mais il y a aussi des prétendants particulièrement mauvais (par exemple OCZ OCZ21066NEW_BT1G) jusqu'à ~ 20%.
Bien que cela ne puisse pas expliquer la cause des ~ 40% d'écriture/lecture signalés, car le code de référence et la configuration utilisés sont probablement différents (le les notes sont vagues ), c'est certainement un facteur . (Je voudrais exécuter certains programmes de référence existants et voir si les chiffres correspondent à ceux du code affiché dans la question.)
Mettre à jour:
J'ai téléchargé la table de recherche de mémoire à partir du site lié et l'ai traitée dans Excel. Bien qu'il affiche toujours une large plage de valeurs , il est beaucoup moins sévère que la réponse d'origine ci-dessus qui ne portait que sur les puces de mémoire les plus lues et quelques-unes entrées "intéressantes" sélectionnées dans les graphiques. Je ne sais pas pourquoi les écarts, en particulier dans les prétendants terribles mentionnés ci-dessus, ne sont pas présents dans la liste secondaire.
Cependant, même sous les nouveaux chiffres, la différence varie encore largement de 50% à 100% (médiane 65, moyenne 65) des performances de lecture. Notez que le fait qu'une puce soit "100%" efficace dans un rapport d'écriture/lecture ne signifie pas qu'elle était meilleure dans l'ensemble .. juste qu'elle était plus équilibrée entre les deux opérations.
Voici mon hypothèse de travail. Si elle est correcte, elle explique pourquoi les écritures sont environ deux fois plus lentes que les lectures:
Même si memset
n'écrit que dans la mémoire virtuelle, en ignorant son contenu précédent, au niveau matériel, l'ordinateur ne peut pas écrire purement sur la DRAM: il lit le contenu de la DRAM dans le cache, les modifie à cet endroit puis les écrit retour à DRAM. Par conséquent, au niveau matériel, memset
fait à la fois la lecture et l'écriture (même si la première semble inutile)! D'où la différence de vitesse à peu près double.
Parce que pour vous lire, il vous suffit d'impulser les lignes d'adresse et de lire les états de base sur les lignes de détection. Le cycle de réécriture se produit après la livraison des données à la CPU et ne ralentit donc pas les choses. D'autre part, pour écrire, vous devez d'abord effectuer une fausse lecture pour réinitialiser les cœurs, puis effectuer le cycle d'écriture.
(Juste au cas où ce ne serait pas évident, cette réponse est ironique - décrivant pourquoi l'écriture est plus lente que la lecture sur une ancienne boîte de mémoire de base.)