web-dev-qa-db-ja.com

メモリへの書き込みが読み取りよりもはるかに遅いのはなぜですか?

次に、単純な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);
}

シングルDDR3-1600メモリモジュールを搭載した私のシステム(詳細は以下)では、次のように出力されます。

帯域幅= 4.751 GB/s(ギガ= 10 ^ 9)

これは理論上の37%ですRAM速度:1.6 GHz * 8 bytes = 12.8 GB/s

一方、同様の「読み取り」テストは次のとおりです。

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

それは出力します:

帯域幅= 11.516 GB/s(ギガ= 10 ^ 9)

大規模な配列のXORを実行するなど、読み取りパフォーマンスの理論上の限界に近づくことができますが、書き込みがはるかに遅いようです。どうして?

[〜#〜] os [〜#〜] Ubuntu 14.04 AMD64(gcc -O3でコンパイルします。-O3 -march=nativeを使用すると、読み取りパフォーマンスが若干低下しますが、memsetには影響しません)

[〜#〜] cpu [〜#〜] Xeon E5-2630 v2

[〜#〜] ram [〜#〜]単一の「16GB PC3-12800パリティREG CL11 240ピンDIMM」(箱に記載されているもの)単一のDIMMを使用するとパフォーマンスが向上すると思いますより予測可能。 4つのDIMMを使用すると、memset最大で4倍高速になると想定しています。

マザーボード Supermicro X9DRG-QF(4チャネルメモリをサポート)

その他のシステム:4GBのDDR3-1067 RAMが2つ搭載されたラップトップ:読み取りと書き込みはどちらも約5.5 GB /秒、ただし、2つのDIMMを使用することに注意してください。

P.S。memsetをこのバージョンで置き換えると、まったく同じパフォーマンスになります

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

あなたのプログラムで、私は得ます

(write) Bandwidth =  6.076 GB/s
(read)  Bandwidth = 10.916 GB/s

6 GBの2GB DIMMを搭載したデスクトップ(Core i7、x86-64、GCC 4.9、GNU libc 2.19)マシン上(申し訳ありませんが、それ以上の詳細はありません)。

ただし、thisプログラムは、書き込み帯域幅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 */

魔法はすべて_mm_stream_si128、別名マシン命令movntdqにあり、16バイトの量をシステムRAMに書き込み、キャッシュをバイパスします(これの公式用語は「 非時間的ストア 」です)。これは、パフォーマンスの違いがすべてキャッシュの動作に関するものであることであることをかなり明確に示していると思います。

N.B. glibc 2.19doesは手作業で最適化されたmemsetを持ち、ベクトル命令を利用します。ただし、非一時的なストアは使用されません。それはおそらくmemsetにとって正しいことです。一般に、メモリを使用する直前にメモリをクリアするので、wantしてキャッシュでホットにします。 (私はさらに賢いmemsetreally hugeブロックの非時間的ストアに切り替わる可能性があると仮定します。キャッシュがそれほど大きくないため、キャッシュにそれらすべてを必要とする可能性があります。)

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   

(これはプログラム自体ではなくlibc.so.6にあります-memsetのアセンブリをダンプしようとした他の人はPLTエントリを見つけただけのようです。アセンブリのダンプを取得する最も簡単な方法はUnixyシステムの実際のmemset

$ 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

パフォーマンスの主な違いは、PC /メモリ領域のキャッシュポリシーにあります。メモリから読み取り、データがキャッシュにない場合、データを使用して計算を実行する前に、メモリをメモリバス経由でキャッシュにフェッチする必要があります。ただし、メモリに書き込む場合、さまざまな書き込みポリシーがあります。ほとんどの場合、システムはライトバックキャッシュ(より正確には「書き込み割り当て」)を使用しています。つまり、キャッシュにないメモリロケーションに書き込む場合、データは最初にメモリからキャッシュにフェッチされ、最終的に書き込まれます。データがキャッシュから削除されたときにメモリに戻ります。これは、データのラウンドトリップと書き込み時のバス帯域幅の2倍の使用を意味します。また、ライトスルーキャッシュポリシー(または「書き込み禁止割り当て」)もあります。これは、書き込みでのキャッシュミス時にデータがキャッシュにフェッチされないことを意味し、読み取りと読み取りの両方で同じパフォーマンスに近いはずです。書き込みます。

28
JarkkoL

違いは、少なくとも私のマシンで、AMDプロセッサーを使用している場合、読み取りプログラムがベクトル化された演算を使用していることです。 2つを逆コンパイルすると、書き込みプログラムで次のようになります。

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>

しかし、これは読書プログラムにとって:

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

また、「自作」のmemsetは実際には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

memsetがベクトル化された演算を使用するかどうかに関する参照を見つけることができません。memset@pltの逆アセンブルは役に立ちません:

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>

この質問 は、memsetがすべてのケースを処理するように設計されているため、一部の最適化が欠落している可能性があることを示唆しています。

この男 は、SIMD命令を利用するには独自のアセンブラmemsetをロールする必要があると確信しているようです。 この質問も行います

暗闇の中で撮影して、SIMD演算を使用していないことを推測します。これは、1つのベクトル化演算のサイズの倍数であるもので動作するかどうか、または何らかの整列があるかどうかを判断できないためです。関連の問題。

ただし、cachegrindを確認すると、キャッシュ効率の問題ではないではないことが確認できます。書き込みプログラムは以下を生成します。

==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%  )

読み取りプログラムは以下を生成します:

==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%  )

読み取りプログラムは、より多くの読み取り(XOR操作ごとに追加の読み取り)を実行するため、LLミス率は低くなりますが、ミスの総数は同じです。したがって、問題が何であれ、それはありません。

16
Patrick Collins

キャッシングと局所性は、ほぼ確実に、あなたが見ているほとんどの影響を説明します。

非決定的なシステムが必要でない限り、書き込みにキャッシュや局所性はありません。ほとんどの書き込み時間は、データがストレージメディア(ハードドライブまたはメモリチップ)に到達するまでの時間として測定されますが、読み取りは、記憶媒体。

9
Robert Harvey

それは(システム全体としての)パフォーマンスの正確な方法かもしれません。読み取りは高速です 一般的な傾向のように見えます広い範囲の相対スループットパフォーマンス。リストされているDDR3 IntelおよびDDR2チャートのクイック分析では、(write /読み取り)%;

一部の高性能DDR3チップは、読み取りスループットの約60〜70%で書き込みを行っています。ただし、一部のメモリモジュール(つまり、ゴールデンエンパイアCL11-13-13 D3-2666)は、書き込みが最大で30%しかありません。

パフォーマンスの高いDDR2チップは、読み取りに比べて書き込みスループットが約50%しかないようです。しかし、20%までのいくつかの著しく悪い候補(OCZ OCZ21066NEW_BT1G)もあります。

使用されているベンチマークコードと設定が異なる可能性があるため、これは報告された〜40%の書き込み/読み取りのtheの原因を説明しない可能性があります( ノートはあいまいです )、これは間違いなくa要因です。 (私はいくつかの既存のベンチマークプログラムを実行し、その数が質問に投稿されたコードの数と一致するかどうかを確認します。)


更新:

リンク先のサイトからメモリ参照表をダウンロードしてExcelで処理しました。それでも値の広い範囲が表示されますが、上の読み取りメモリチップといくつかのチャートから「興味深い」エントリを選択しました。特に上記で特定された恐ろしい候補の差異が、セカンダリリストに含まれていない理由がわかりません。

ただし、新しい数値でも、読み取りパフォーマンスの50%〜100%(中央値65、平均65)の範囲は依然として大きく異なります。チップが書き​​込み/読み取り比率で「100%」効率的だったからといって、全体的に優れているとは限らないことに注意してください。2つの操作の間でより均一であったというだけです。

6
user2864740

これが私の作業仮説です。正しい場合は、書き込みが読み取りよりも約2倍遅い理由を説明します。

memsetは、以前の内容を無視して仮想メモリにのみ書き込みますが、ハードウェアレベルでは、DRAMへの純粋な書き込みを行うことはできません。DRAMの内容をキャッシュに読み込み、そこで変更してから書き込みます。 DRAMに戻る。したがって、ハードウェアレベルでは、memsetは読み取りと書き込みの両方を実行します(前者は役に立たないように見えますが)。したがって、おおよそ2倍の速度差があります。

4
MaxB

読み取るには、単にアドレスラインをパルスし、センスラインのコア状態を読み取るためです。ライトバックサイクルは、データがCPUに配信された後に発生するため、処理速度が低下することはありません。一方、書き込みを行うには、まず偽の読み取りを実行してコアをリセットしてから、書き込みサイクルを実行する必要があります。

(それが明白でない場合に備えて、この答えはほのぼのです-なぜ書き込みが古いコアメモリボックスで読み取るより遅いのかを説明しています。)

2
Hot Licks