programing

왜 기억에 쓰는 것이 읽는 것보다 훨씬 느릴까요?

coolbiz 2022. 7. 7. 23:42
반응형

왜 기억에 쓰는 것이 읽는 것보다 훨씬 느릴까요?

간단한 .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 메모리 모듈을 1개 탑재한 시스템(상세)에서는 다음과 같이 출력됩니다.

대역폭 = 4.751 GB/s (기가 = 10^9)

속도의 입니다.「 RAM 」 、 「 37% 」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 A 싱글 "16GB PC3-12800 패리티 REG CL11 240 핀 DIMM" (상자에 기재되어 있는 내용)DIMM 을 1 개 탑재하면, 퍼포먼스의 예측성이 높아집니다.DIMM이 4개 있으면memset속도가 최대 4배 빨라집니다.

메인보드 Supermicro X9DRG-QF (4채널 메모리 지원)

추가 시스템:DDR3-1067 RAM을 2대 탑재한 노트북: 읽기 및 쓰기 모두 약 5.5GB/s이지만 DIMM은 2개 사용.

추신 교환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;
}

당신의 프로그램을 통해

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

데스크톱(Core i7, x86-64, GCC 4.9, GNU libc 2.19) 머신에 2GB DIMM이 6개 탑재되어 있습니다(그 이상의 자세한 내용은 없습니다만, 죄송합니다).

단, 이 프로그램에서는 쓰기 대역폭이 다음과 같은 대역폭이 보고됩니다.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캐시를 바이패스하여 시스템 RAM에 16바이트의 양을 씁니다(이것에 대한 공식 용어는 "비임시 저장소").이는 성능의 차이가 캐시 동작에 달려 있다는 것을 매우 확실하게 보여주는 것이라고 생각합니다.

N.B. glibc 2.19는 정교하게 손으로 최적화된memset벡터 명령을 사용합니다.단, 비임시 스토어는 사용하지 않습니다.이 점이 가장 적합할 것 같습니다.memset일반적으로 메모리 사용 직전에 메모리를 클리어하기 때문에 캐시 내에서 핫 상태로 만들어야 합니다.(더 똑똑한 사람은memset캐시의 크기가 그다지 크지 않기 때문에 캐시에 모든 것을 저장할 수 없다는 이론으로 인해 블록 클리어에 대해 비표준 저장소로 전환할 수 있습니다.)

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 - " 했던 "어셈블리를 덤프하려고 .memsetPLT를 사용하다 memset에서 Unixy는

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

.)

성능의 주요 차이는 PC/메모리 영역의 캐싱 정책에 있습니다.메모리에서 읽고 데이터가 캐시에 없는 경우 데이터를 계산하려면 먼저 메모리 버스를 통해 메모리를 캐시에 가져와야 합니다.그러나 메모리에 쓸 때는 다른 쓰기 정책이 있습니다.시스템에서 라이트백 캐시(또는 보다 정확하게는 "쓰기 할당")를 사용하고 있을 가능성이 높습니다.즉, 캐시에 없는 메모리 위치에 데이터를 쓸 때 데이터가 먼저 메모리에서 캐시로 가져오고 데이터가 캐시에서 제거되면 최종적으로 메모리에 다시 쓰이게 됩니다.즉, 데이터는 왕복으로 2배의 버스 대역wi를 의미합니다.d번째 쓰기 사용률.또한 라이트 스루 캐싱 정책(또는 "쓰기 없음 할당")도 있습니다. 이는 일반적으로 쓰기 시 캐시 누락 시 데이터가 캐시에 가져오기되지 않으므로 읽기 및 쓰기 모두에서 동일한 성능에 근접할 수 있음을 의미합니다.

적어도 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는, 는 「Call」, 「Call」에의되어 있습니다.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모든 케이스를 처리하도록 설계되어 있어 최적화가 누락되어 있을 수 있습니다.

남자는 당신이 직접 조립자를 굴려야 한다고 확신하고 있는 것 같아요memsetSIMD 명령을 활용할 수 있습니다.이 질문도 그렇습니다.

SIMD 연산을 사용하지 않는 이유는 SIMD 연산이 하나의 벡터화된 연산의 배수로 동작하는지 또는 정렬과 관련된 문제가 있는지 여부를 알 수 없기 때문입니다.

그러나 캐시 효율의 문제가 아님을 확인하려면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%  )

더읽기를 (읽기 빈도가 더 높아짐) LL ( 빈도가 더 높아짐)은 LL 손실률(읽기 빈도가 더 높아짐)이 합니다.XORoperationoperation).그래서 문제가 무엇이든 간에, 그것은 거기에 없습니다.

캐싱과 로컬은 대부분의 효과를 확실하게 설명합니다.

비결정적 시스템을 원하지 않는 한 쓰기에는 캐싱이나 로컬리티가 없습니다.대부분의 쓰기 시간은 데이터가 스토리지 미디어(하드 드라이브 또는 메모리 칩)에 도달하는 데 걸리는 시간으로 측정되지만 읽기 속도는 스토리지 미디어보다 빠른 캐시 레이어 수에 관계없이 가능합니다.

(System-as-a-Whole)의 동작 방식일 수 있습니다.읽기 속도는 상대적인 스루풋 퍼포먼스의 범위가 넓은 일반적인 경향인 것 같습니다.(쓰기/읽기)%의 일부 케이스로서 기재되어 있는 DDR3 및 DDR2 차트를 간단하게 분석한다.

일부 최고 성능의 DDR3 칩은 읽기 스루풋의 약 60~70%로 쓰기 가능합니다.다만, 메모리 모듈(즉,Golden Empiry CL11-13-13 D3-2666)의 기입률은 최대 30%에 불과합니다.

최고 성능의 DDR2 칩은 읽기 대비 쓰기 스루풋의 약 50%밖에 되지 않습니다.하지만 눈에 띄게 나쁜 경쟁자도 있습니다.OCZ OCZ21066NEW_BT1G)를 최대 20% 절감.

이것이 최대 40%의 쓰기/읽기 보고의 원인을 설명하지는 못할 수 있지만 벤치마크 코드와 사용하는 설정이 다를 수 있습니다(주석이 모호함). 이는 분명 한 요인입니다.(기존 벤치마크 프로그램을 실행하여 질문에 게재된 코드의 수치와 일치하는지 확인합니다.)


업데이트:

링크 사이트에서 메모리 룩업 테이블을 다운받아 엑셀로 처리했습니다.여전히 광범위한 값을 보여주지만, 위의 답변에서는 상위 읽기 메모리 칩과 차트에서 선택된 몇 개의 "흥미로운" 항목만 살펴본 것에 비해 훨씬 덜 심각합니다.나는 왜 특히 위에서 뽑은 끔찍한 경쟁자들에서의 불일치가 2차 목록에 없는지 잘 모르겠다.

그러나 새로운 수치에서도 읽기 성능의 50%~100%(중간 65, 평균 65)의 차이가 여전히 크게 납니다.칩의 기입/읽기 효율이 「100%」라고 해서, 전체적으로 뛰어난 것은 아닙니다.단지 두 작전 사이에 평온한 관계가 될 뿐이었다.

제 작업 가설은 이렇습니다.올바른 경우, 다음과 같은 읽기보다 쓰기가 약 2배 느린 이유를 설명합니다.

그럼에도 불구하고.memset가상 메모리에 쓸 뿐 이전 내용은 무시한 채 하드웨어 수준에서 컴퓨터는 DRAM에 순수하게 쓸 수 없습니다. DRAM을 사용하다에서는 " " " 입니다.memset읽기와 쓰기를 모두 한다(전자는 쓸모없어 보이지만).2번으로 나누다

왜냐하면 읽기 위해서는 단순히 주소 라인을 펄스화하고 감지 라인의 핵심 상태를 읽기 때문입니다.기입 사이클은 데이터가 CPU로 전달된 후에 발생하므로 속도가 느려지지 않습니다.한편, 쓰려면 먼저 가짜 읽기를 수행하여 코어를 리셋한 다음 쓰기 사이클을 수행해야 합니다.

(분명하지 않은 경우를 대비해서, 이 답변은 빈말입니다.왜 쓰기가 오래된 코어 메모리 박스에서 읽는 것보다 느린지 설명합니다.)

언급URL : https://stackoverflow.com/questions/25827416/why-is-writing-to-memory-much-slower-than-reading-it

반응형