web-dev-qa-db-fra.com

Pourquoi l'écriture dans la mémoire est-elle beaucoup plus lente que la lecture?

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;
}
48
MaxB

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
...

.)

43
zwol

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.

28
JarkkoL

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à.

16
Patrick Collins

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.

9
Robert Harvey

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.

6
user2864740

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.

4
MaxB

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.)

2
Hot Licks