Почему писать в память гораздо медленнее, чем читать?
Вот простой тег пропускной способности 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 ядра, а затем выполнить цикл записи.
(На всякий случай, это не очевидно, этот ответ является языком в щеке - описание того, почему запись медленнее, чем чтение на старой коробке памяти ядра.)