Существуют ли современные / древние процессоры / микроконтроллеры, в которых хранилище кэшированных байтов на самом деле медленнее, чем хранилище слов?
Общепринято утверждать, что хранение байтов в кеше может привести к внутреннему циклу чтения-изменения-записи или иным образом повредить пропускную способность или задержку по сравнению с сохранением полного регистра.
Но я никогда не видел примеров. Никакие процессоры x86 не являются такими, и я думаю, что все высокопроизводительные процессоры также могут напрямую изменять любой байт в строке кэша. Отличаются ли некоторые микроконтроллеры или младшие процессоры, если они вообще имеют кеш?
(Я не считаю машин с адресацией по словам или Alpha, которая адресуется в байтах, но не содержит инструкций загрузки / сохранения в байтах. Я говорю о самой узкой инструкции сохранения, которую ISA изначально поддерживает.)
В моем исследовании, отвечая на вопрос, может ли современное аппаратное обеспечение x86 не хранить в памяти ни одного байта? Я обнаружил, что причины, по которым Alpha AXP опускает хранилища байтов, предполагают, что они будут реализованы как истинные хранилища байтов в кеше, а не как обновление RMW содержащего слова. (Таким образом, это сделало бы защиту ECC для L1d-кэша более дорогой, поскольку вместо 32-битной требовалась бы гранулярность байтов).
Все современные архитектуры (кроме ранней Alpha) могут выполнять истинную загрузку / сохранение байтов в не кэшируемую память (не циклы RMW), что необходимо для записи драйверов устройств для устройств, которые имеют смежные байтовые регистры ввода / вывода. (например, с помощью внешних сигналов включения / выключения, чтобы указать, какие части более широкой шины содержат реальные данные, такие как 2-битный TSIZ (размер передачи) на этом CPU/ микроконтроллере ColdFire, или как однобайтовые передачи PCI / PCIe, или как DDR Управляющие сигналы SDRAM, которые маскируют выбранные байты.)
Может быть, создание цикла RMW в кеше для хранилищ байтов было бы чем-то, что следует учитывать при разработке микроконтроллера, даже если это не высокопроизводительный суперскалярный конвейерный дизайн, предназначенный для серверов / рабочих станций SMP, таких как Alpha?
Я думаю, что это утверждение может исходить от адресных машин. Или из невыровненных 32-битных хранилищ, требующих множественного доступа ко многим процессорам, и людей, которые неправильно обобщают данные из хранилищ байтов.
Просто для ясности, я ожидаю, что цикл хранения байтов с тем же адресом будет выполняться с теми же циклами на итерации, что и цикл хранения слов. Таким образом, для заполнения массива 32-битные хранилища могут увеличиться в 4 раза быстрее, чем 8-битные. (Возможно, меньше, если 32-битные хранилища насыщают пропускную способность памяти, а 8-битные хранилища - нет.) Но если у байтовых хранилищ нет дополнительного штрафа, вы не получите более чем 4-кратную разницу в скорости. (Или какова бы ни была ширина слова).
И я говорю об асме. Хороший компилятор будет автоматически векторизовать цикл хранения байтов или int в C и использовать более широкие хранилища или то, что оптимально для целевой ISA.
; x86-64 NASM syntax
mov rdi, rsp
; RDI holds at a 32-bit aligned address
mov ecx, 1000000000
.loop: ; do {
mov byte [rdi], al
mov byte [rdi+2], dl ; store two bytes in the same dword
; no pointer increment, this is the same 32-bit dword every time
dec ecx
jnz .loop ; }while(--ecx != 0}
mov eax,60
xor edi,edi
syscall ; x86-64 Linux sys_exit(0)
Или цикл над массивом 8 КБ, подобный этому, сохраняющий 1 байт или 1 слово из каждых 8 байт (для реализации C с sizeof(unsigned int)=4 и CHAR_BIT=8 для 8 КБ, но он должен компилироваться в сопоставимые функции на любом Реализация C, с незначительным смещением, если sizeof(unsigned int)
не сила 2). ASM на Godbolt для нескольких разных ISA, либо без развертывания, либо с одинаковым количеством развертываний для обеих версий.
// volatile defeats auto-vectorization
void byte_stores(volatile unsigned char *arr) {
for (int outer=0 ; outer<1000 ; outer++)
for (int i=0 ; i< 1024 ; i++) // loop over 4k * 2*sizeof(int) chars
arr[i*2*sizeof(unsigned) + 1] = 123; // touch one byte of every 2 words
}
// volatile to defeat auto-vectorization: x86 could use AVX2 vpmaskmovd
void word_stores(volatile unsigned int *arr) {
for (int outer=0 ; outer<1000 ; outer++)
for (int i=0 ; i<(1024 / sizeof(unsigned)) ; i++) // same number of chars
arr[i*2 + 0] = 123; // touch every other int
}
Регулируя размеры по мере необходимости, мне было бы очень любопытно, если бы кто-нибудь мог указать на систему, где word_store()
быстрее чем byte_store()
, (Если на самом деле бенчмаркинг, остерегайтесь эффектов прогрева, таких как динамическая тактовая частота, и первый прогон, запускающий TLB и кэширование.)
Или, если фактические компиляторы C для древних платформ не существуют или генерируют неоптимальный код, который не ограничивает пропускную способность магазина, то любой asm, созданный вручную, мог бы показать эффект.
Любой другой способ продемонстрировать замедление для хранилищ байтов - это хорошо, я не настаиваю на зацикленных циклах над массивами или спам-записях в одном слове.
Я также нашел бы подробную документацию о внутренних процессорах ЦП или числах тактов ЦП для различных инструкций. Я опасаюсь советов по оптимизации или руководств, которые могут быть основаны на этом утверждении, хотя и не были протестированы.
- Любой по-прежнему актуальный процессор или микроконтроллер, где хранилища кэшированных байтов имеют дополнительный штраф?
- Любой по-прежнему актуальный процессор или микроконтроллер, где не кэшируемые хранилища байтов имеют дополнительные штрафы?
- Любые не все еще актуальные исторические процессоры (с или без кэшей обратной записи или сквозной записи), где любое из вышеперечисленного верно? Какой самый последний пример?
Например, так ли это на ARM Cortex-A?? или кортекс-м? Любая старая микроархитектура ARM? Какой-нибудь микроконтроллер MIPS или ранний процессор / серверная рабочая станция MIPS? Что-нибудь другое случайное RISC, как PA-RISC, или CISC, как VAX или 486? (CDC6600 был адресуемым словом.)
Или создайте контрольный пример, включающий как нагрузки, так и хранилища, например, показывая слово-RMW из хранилищ байтов, конкурирующих с пропускной способностью нагрузки.
(Мне не интересно показывать, что пересылка из хранилищ байтов в загрузки слов медленнее, чем word->word, потому что нормально, что SF работает эффективно только тогда, когда загрузка полностью содержится в самом последнем хранилище, чтобы коснуться любого из соответствующие байты. Но то, что показало бы, что переадресация байтов-> байтов менее эффективна, чем слово-> слово SF, было бы интересно, возможно, с байтами, которые не начинаются с границы слова.)
(Я не упомянул загрузку байтов, потому что это обычно просто: получить доступ к полному слову из кэша или ОЗУ, а затем извлечь нужный байт. Эти детали реализации неотличимы, кроме как для MMIO, где ЦП определенно не читают содержащее слово.)
В архитектуре загрузки / хранения, такой как MIPS, работа с байтовыми данными означает, что вы используете lb
или же lbu
загрузить и обнулить или подписать-расширить его, а затем сохранить его с sb
, (Если вам нужно усечение до 8 бит между шагами в регистрах, тогда вам может потребоваться дополнительная инструкция, поэтому обычно локальные переменные должны иметь размер регистра. Если вы не хотите, чтобы компилятор автоматически векторизовал SIMD с 8-битными элементами, то часто uint8_t Локальные данные хороши...) Но в любом случае, если вы делаете это правильно, а ваш компилятор работает хорошо, не нужно никаких дополнительных инструкций, чтобы иметь байтовые массивы.
Я заметил, что GCC имеет sizeof(uint_fast8_t) == 1
на ARM, AArch64, x86 и MIPS. Но ИДК, сколько акций мы можем положить в это. X86-64 System V ABI определяет uint_fast32_t
как 64-битный тип на x86-64. Если они собираются это сделать (вместо 32-битного размера по умолчанию для x86-64), uint_fast8_t
также должен быть 64-битным типом. Может быть, чтобы избежать нулевого расширения при использовании в качестве индекса массива? Если он был передан как функция arg в регистр, так как он может быть бесплатно расширен до нуля, если вам все равно придется загружать его из памяти.
2 ответа
Документация ARM для Cortex-A15 MPCore (с ~2012 г.) гласит, что она использует 32-битную гранулярность ECC в L1d и фактически выполняет слово RMW для узких хранилищ для обновления данных.
Кэш данных L1 поддерживает необязательную однобитовую корректную и двухбитную логику коррекции ошибок обнаружения как в тегах, так и в массивах данных. Гранулярность ECC для массива тегов - это тег для отдельной строки кэша, а гранулярность ECC для массива данных - это 32-разрядное слово.
Из-за гранулярности ECC в массиве данных запись в массив не может обновить часть 4-байтовой выровненной ячейки памяти, поскольку недостаточно информации для вычисления нового значения ECC. Это относится к любой инструкции сохранения, которая не записывает одну или несколько выровненных 4-байтовых областей памяти. В этом случае система памяти данных L1 считывает существующие данные в кеше, объединяет измененные байты и вычисляет ECC на основе объединенного значения. Система памяти L1 пытается объединить несколько хранилищ вместе, чтобы соответствовать выровненной 4-байтовой гранулярности ECC и избежать требования чтения-изменения-записи.
(Когда они говорят "система памяти L1", я думаю, что они имеют в виду буфер хранилища, если у вас есть смежные хранилища байтов, которые еще не зафиксировали L1d.)
Я стою исправлено. Может быть штраф за узкие магазины на некоторых не x86 процессорах.
Cortex-A15 MPCore - это трёхсторонний исполнительный ЦП, работающий не по порядку, так что это не минимальная мощность / простая конструкция ARM, но они решили потратить транзисторы на OoO exec, но не на эффективные хранилища байтов.
Предположительно без необходимости поддерживать эффективные невыровненные хранилища (которые программное обеспечение для x86 более вероятно примет / воспользуется), более медленные хранилища байтов считались оправданными для более высокой надежности ECC для L1d без чрезмерных издержек.
Cortex-A15, вероятно, не единственное и не самое последнее ядро ARM, работающее таким образом.
cortex-m7 trm, раздел кэша оперативной памяти руководства.
В безошибочной системе основное влияние на производительность оказывает стоимость схемы чтения-изменения-записи для неполных хранилищ на стороне данных. Если слот буфера хранения не содержит хотя бы полного 32-битного слова, он должен прочитать слово, чтобы иметь возможность вычислить контрольные биты. Это может произойти из-за того, что программное обеспечение выполняет запись только в область памяти с инструкциями по хранению байтов или полуслов. Затем данные могут быть записаны в ОЗУ. Это дополнительное чтение может оказать негативное влияние на производительность, поскольку оно не позволяет использовать слот для другой записи.
,
Буферизация и выдающиеся возможности системы памяти маскируют часть дополнительного чтения, и она незначительна для большинства кодов. Однако ARM рекомендует использовать как можно меньше кэшируемых инструкций STRB и STRH, чтобы уменьшить влияние на производительность.
У меня есть Cortex-M7s, но до сих пор не выполнили тест, чтобы продемонстрировать это.
Под словом "читать слово" подразумевается чтение одного места хранения в SRAM, которое является частью кеша данных. Это не системная память высокого уровня.
Внутренние части кеша построены из блоков SRAM и вокруг них, которые являются быстрой SRAM, которая делает кеш тем, что он есть, быстрее, чем системная память, быстро возвращает ответы обратно процессору и т. Д. Это чтение-изменение-запись (RMW) не вещь политики записи высокого уровня. Они говорят, что если есть попадание и политика записи говорит, что нужно сохранить запись в кеше, тогда байт или полуслово должны быть записаны в одну из этих SRAM. Ширина данных SRAM кэша данных с ECC, как показано в этом документе, составляет 32 + 7 бит. 32 бита данных 7 битов контрольных битов ECC. Вы должны держать все 39 бит вместе, чтобы ECC работал. По определению вы не можете изменить только некоторые биты, так как это приведет к ошибке ECC.
Всякий раз, когда необходимо изменить любое количество битов в этом 32-битном слове, хранящемся в данных SRAM кэша данных, 8, 16 или 32 бита, необходимо пересчитать 7 контрольных битов и записать все 39 битов одновременно. Для 8- или 16-битной записи STRB или STRH необходимо прочитать 32 бита данных, изменив 8 или 16 битов, оставив биты данных в этом слове неизменными, 7 проверенных битов ECC и 39 битов, записанных в sram,
Вычисление контрольных битов идеально / вероятно выполняется в одном и том же тактовом цикле, который устанавливает запись, но чтение и запись не находятся в одном и том же тактовом цикле, поэтому для записи данных, поступивших в кэш, должно потребоваться как минимум два отдельных цикла. за один такт Существуют приемы, чтобы задержать запись, которая иногда также может повредить, но обычно перемещает ее в цикл, который был бы неиспользован и делает его свободным, если хотите. Но это не будет тот же тактовый цикл, что и чтение.
Они говорят, что если вы будете держать язык за зубами и сумеете получить достаточное количество небольших магазинов, то достаточно быстро попадете в кеш, они остановят процессор, пока не смогут его догнать.
Документ также описывает без ECC SRAM ширину 32 бита, что подразумевает, что это также верно при компиляции ядра без поддержки ECC. У меня нет доступа ни к сигналам для этого интерфейса памяти, ни к документации, поэтому я не могу сказать наверняка, но если он реализован как 32-битный интерфейс без управления байтовой дорожкой, то у вас возникает та же проблема, он может записать только 32-битный элемент к этой SRAM, а не к дробным частям, поэтому для замены 8 или 16 битов необходимо использовать RMW в недрах кэша.
Короткий ответ, почему бы не использовать более узкую память, это размер чипа, при этом ECC удваивает размер, поскольку существует ограничение на количество проверочных битов, которые вы можете использовать даже при уменьшении ширины (7 бит на каждые 8 бит намного больше биты для сохранения, чем 7 бит на каждые 32). Чем уже память, тем больше у вас сигналов для маршрутизации и вы не можете упаковать память настолько плотно. Квартира против группы отдельных домов, чтобы вместить одинаковое количество людей. Дороги и тротуары к входной двери вместо прихожих.
И особенно с таким одноядерным процессором, если только вы намеренно не попробуете (что я и сделаю), маловероятно, что вы случайно столкнетесь с этим, и зачем повышать стоимость продукта: возможно, этого не произойдет?
Обратите внимание, что даже с многоядерным процессором вы увидите память, созданную таким образом.
РЕДАКТИРОВАТЬ.
Ладно дошли до теста.
0800007c <lwtest>:
800007c: b430 push {r4, r5}
800007e: 6814 ldr r4, [r2, #0]
08000080 <lwloop>:
8000080: 6803 ldr r3, [r0, #0]
8000082: 6803 ldr r3, [r0, #0]
8000084: 6803 ldr r3, [r0, #0]
8000086: 6803 ldr r3, [r0, #0]
8000088: 6803 ldr r3, [r0, #0]
800008a: 6803 ldr r3, [r0, #0]
800008c: 6803 ldr r3, [r0, #0]
800008e: 6803 ldr r3, [r0, #0]
8000090: 6803 ldr r3, [r0, #0]
8000092: 6803 ldr r3, [r0, #0]
8000094: 6803 ldr r3, [r0, #0]
8000096: 6803 ldr r3, [r0, #0]
8000098: 6803 ldr r3, [r0, #0]
800009a: 6803 ldr r3, [r0, #0]
800009c: 6803 ldr r3, [r0, #0]
800009e: 6803 ldr r3, [r0, #0]
80000a0: 3901 subs r1, #1
80000a2: d1ed bne.n 8000080 <lwloop>
80000a4: 6815 ldr r5, [r2, #0]
80000a6: 1b60 subs r0, r4, r5
80000a8: bc30 pop {r4, r5}
80000aa: 4770 bx lr
есть загрузочное слово (ldr), загрузочный байт (ldrb), варианты хранения слова (str) и хранения байтов (strb), каждая из которых выровнена по крайней мере на 16-байтовых границах вплоть до вершины адреса цикла.
с включенным icache и dcache
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0001000B
00010007
0001000B
00010007
0001000C
00010007
0002FFFD
0002FFFD
нагрузки соответствуют друг другу, как и ожидалось, однако магазины, когда вы их собираете таким образом, записывают байты в 3 раза дольше, чем запись слова.
но если вы не попали в кэш, что трудно
0800019c <nbtest>:
800019c: b430 push {r4, r5}
800019e: 6814 ldr r4, [r2, #0]
080001a0 <nbloop>:
80001a0: 7003 strb r3, [r0, #0]
80001a2: 46c0 nop ; (mov r8, r8)
80001a4: 46c0 nop ; (mov r8, r8)
80001a6: 46c0 nop ; (mov r8, r8)
80001a8: 7003 strb r3, [r0, #0]
80001aa: 46c0 nop ; (mov r8, r8)
80001ac: 46c0 nop ; (mov r8, r8)
80001ae: 46c0 nop ; (mov r8, r8)
80001b0: 7003 strb r3, [r0, #0]
80001b2: 46c0 nop ; (mov r8, r8)
80001b4: 46c0 nop ; (mov r8, r8)
80001b6: 46c0 nop ; (mov r8, r8)
80001b8: 7003 strb r3, [r0, #0]
80001ba: 46c0 nop ; (mov r8, r8)
80001bc: 46c0 nop ; (mov r8, r8)
80001be: 46c0 nop ; (mov r8, r8)
80001c0: 3901 subs r1, #1
80001c2: d1ed bne.n 80001a0 <nbloop>
80001c4: 6815 ldr r5, [r2, #0]
80001c6: 1b60 subs r0, r4, r5
80001c8: bc30 pop {r4, r5}
80001ca: 4770 bx lr
тогда слово и байт занимают одинаковое количество времени
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0000C00B
0000C007
0000C00B
0000C007
Байт по-прежнему занимает в 4 раза больше времени, чем слова, все остальные факторы остаются постоянными, но это было проблемой, когда байты занимают более чем в 4 раза больше времени.
так как я описывал перед этим вопросом, вы увидите, что размер кэш-памяти является оптимальной шириной в кэше, а также в других местах и при записи байтов будет происходить чтение-изменение-запись. Теперь, является ли это видимым для других издержек или оптимизаций или нет, это отдельная история. ARM четко заявило, что это может быть видно, и я чувствую, что продемонстрировал это. Это ни в коей мере не отрицательно относится к дизайну ARM, на самом деле, наоборот, RISC в целом переходит наверх по мере выполнения инструкций / выполнения, для выполнения той же задачи требуется больше инструкций. Эффективность в дизайне позволяет таким вещам быть видимыми. Есть целые книги, написанные о том, как заставить ваш x86 работать быстрее, не выполнять 8-битные операции для того или другого, или другие инструкции предпочтительны, и т. Д. Это означает, что вы должны быть в состоянии написать тест для демонстрации этих падений производительности. Точно так же, как этот, даже если вы вычисляете каждый байт в строке, когда вы перемещаете его в память, это должно быть скрыто, вам нужно написать код, подобный этому, и если вы собираетесь делать что-то подобное, вы можете записать инструкции, объединяющие байты. в слове, прежде чем делать запись, может быть или не быть быстрее... зависит.
Если бы у меня было halfword (strh), то неудивительно, что он также переносит чтение-модификацию-запись, поскольку оперативная память имеет ширину 32 бита (плюс любые ecc-биты, если таковые имеются)
0001000C str
00010007 str
0002FFFD strh
0002FFFD strh
0002FFFD strb
0002FFFD strb
нагрузки занимают столько же времени, сколько ширина sram считывается целиком и помещается на шину, процессор извлекает из этого интересующие байтовые дорожки, так что это не требует затрат времени и часов.