Почему писать в память гораздо медленнее, чем читать?

Вот простой тег пропускной способности 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 ГБ/с (Giga = 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 ГБ/с (Giga = 10 ^ 9)

Я могу приблизиться к теоретическому пределу для производительности чтения, например, XORing большого массива, но запись кажется намного медленнее. Почему?

ОС Ubuntu 14.04 AMD64 (компилируется с помощью gcc -O3. Использование -O3 -march=native делает производительность чтения немного хуже, но не влияет на memset)

CPU Xeon E5-2630 v2

ОЗУ Один "16 ГБ PC3-12800 Parity REG CL11 240-Pin DIMM" (что он говорит на ящике) Я думаю, что наличие единого DIMM делает производительность более предсказуемой. Я предполагаю, что с 4 модулями DIMM memset будет работать в 4 раза быстрее.

Материнская плата Supermicro X9DRG-QF (поддерживает 4-канальную память)

Дополнительная система: ноутбук с 2x 4 ГБ оперативной памяти DDR3-1067: чтение и запись составляют около 5.5 ГБ/с, но обратите внимание, что он использует 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;
}

Ответ 1

С вашими программами я получаю

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

на рабочем столе (Core i7, x86-64, GCC 4.9, GNU libc 2.19) с шестью 2 ГБ модулями DIMM. (У меня нет более подробной информации, чем рука, извините.)

Однако эта программа сообщает ширину полосы пропускания 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-байтовое количество в ОЗУ системы, минуя кеш (официальный жаргон для этого "" ). Я думаю, что это довольно убедительно демонстрирует, что разница в производительности связана с поведением кэша.

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, а не сама программа - другой человек, который попытался сбросить сборку для memset, кажется, только нашел свою запись PLT. Самый простой способ получить свалку сборки для реального 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
...

.)

Ответ 2

Основное отличие производительности зависит от политики кэширования вашего ПК/области памяти. Когда вы читаете из памяти, а данные не находятся в кеше, память должна быть сначала извлечена в кеш через шину памяти, прежде чем вы сможете выполнять любые вычисления с данными. Однако при записи в память существуют разные политики записи. Скорее всего, ваша система использует кеш обратной записи (или, точнее, "write allocate" ), что означает, что при записи в ячейку памяти, а не в кеше, данные сначала извлекаются из памяти в кеш и в конечном итоге записываются обратно в память, когда данные выгружаются из кеша, что означает обратную связь для данных и использование пропускной способности шины 2x при записи. Существует также политика кэширования с помощью записи (или "отсутствие записи" ), что обычно означает, что при пропуске кеша при записи данные не извлекаются в кеш и что должно давать более близкую к той же производительности как для чтения, так и для пишет.

Ответ 3

Разница - по крайней мере, на моей машине с процессором AMD - это то, что программа чтения использует векторизованные операции. Декомпиляция двух выходных данных для программы записи:

0000000000400610 <main>:
  ...
  400628:       e8 73 ff ff ff          callq  4005a0 <[email protected]>
  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 <[email protected]>
  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 <[email protected]>

Но это для программы чтения:

00000000004005d0 <main>:
  ....
  400609:       e8 62 ff ff ff          callq  400570 <[email protected]>
  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 <[email protected]>
  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 <[email protected]>
  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 векторизованные операции, дизассемблирование [email protected] здесь бесполезно:

00000000004005b0 <[email protected]>:
  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 предназначен для обработки каждого случая, возможно, ему не хватает некоторых оптимизаций.

Этот парень определенно убежден, что вам нужно свернуть собственный ассемблер memset, чтобы воспользоваться инструкциями 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, потому что она выполняет гораздо больше чтений (дополнительное чтение за операцию XOR), общее количество пропусков одинаково. Так что какова бы ни была проблема, это не так.

Ответ 4

Кэширование и локальность почти наверняка объясняют большую часть эффектов, которые вы видите.

В записи нет кэширования или локальности, если вы не хотите недетерминированной системы. Большинство времени записи измеряются как время, необходимое для того, чтобы данные полностью попадали на носитель данных (будь то жесткий диск или чип памяти), тогда как чтение может исходить из любого количества уровней кеша, которые быстрее, чем носитель информации.

Ответ 5

Это может быть просто, как это делает (система-как-целое). Быстрое чтение является общей тенденцией с широким диапазоном относительной производительности. Вкратце проанализировав перечисленные ниже графики DDR3 Intel и DDR2, как несколько отдельных случаев (запись/чтение)%;

Некоторые высокопроизводительные чипы DDR3 записывают примерно на ~ 60-70% от скорости чтения. Однако есть некоторые модули памяти (т.е. Golden Empire CL11-13-13 D3-2666) до записи всего ~ 30%.

У наиболее производительных DDR2-чипов, по-видимому, есть только около 50% пропускной способности записи по сравнению с чтением. Но есть и некоторые очень плохие соперники (т.е. OCZ OCZ21066NEW_BT1G) до ~ 20%.

Хотя это может и не объяснить причину сообщения о записи/чтении ~ 40%, поскольку используемый в нем тест-код и настройка могут отличаться (примечания являются неопределенными), это определенно фактор. (Я бы запустил некоторые существующие тестовые программы и посмотрел, совпадают ли цифры с данными кода, размещенного в вопросе.)


Update:

Я загрузил таблицу поиска памяти из связанного сайта и обработал ее в Excel. Хотя он по-прежнему показывает широкий диапазон значений, он намного меньше, чем исходный ответ, выше которого были рассмотрены только чипы памяти с высоким уровнем чтения и несколько избранных "интересных" записей из диаграмм. Я не уверен, почему расхождения, особенно в ужасных претендентах, выделенных выше, отсутствуют во вторичном списке.

Однако даже при новых числах разница все еще широко варьируется от 50% -100% (средняя 65, средняя 65) от производительности чтения. Обратите внимание, что только потому, что чип был "100%" эффективен в отношении записи/чтения, это не значит, что он был лучше в целом. Просто он был более ровным килем между двумя операциями.

Ответ 6

Вот моя рабочая гипотеза. Если это правильно, это объясняет, почему записи примерно в два раза медленнее, чем читает:

Несмотря на то, что memset записывает только в виртуальную память, игнорируя ее предыдущее содержимое, на аппаратном уровне компьютер не может выполнить чистую запись в DRAM: он считывает содержимое DRAM в кеш, модифицирует их там, а затем записывает их вернуться к DRAM. Поэтому на аппаратном уровне memset выполняет чтение и запись (хотя первое кажется бесполезным)! Следовательно, примерно двукратная разность скоростей.

Ответ 7

Потому что, чтобы читать, вы просто пульсируете адресные строки и зачитываете основные состояния в линиях чувств. Цикл обратной записи возникает после того, как данные доставляются в CPU и, следовательно, не замедляют работу. С другой стороны, чтобы написать, вы должны сначала выполнить поддельное чтение на reset ядра, а затем выполнить цикл записи.

(На всякий случай, это не очевидно, этот ответ является языком в щеке - описание того, почему запись медленнее, чем чтение на старой коробке памяти ядра.)