Существуют ли какие-либо современные процессоры, где кэшированное хранилище байтов на самом деле медленнее, чем хранилище слов? - PullRequest
0 голосов
/ 16 января 2019

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

Но я никогда не видел примеров. Никакие процессоры x86 не являются такими, и я думаю, что все высокопроизводительные процессоры также могут напрямую изменять любой байт в строке кэша. Отличаются ли некоторые микроконтроллеры или младшие процессоры, если у них вообще есть кеш?

( Я не считаю машин с адресацией слов или Alpha, которая адресуется в байтах, но не содержит инструкций загрузки / сохранения байтов. Я говорю о самой узкой инструкции сохранения, которую ISA изначально поддерживает.)

В своем исследовании, отвечая на вопрос Может ли современное аппаратное обеспечение x86 не хранить один байт в памяти? я обнаружил, что причины, по которым Alpha AXP опускает хранилища байтов, предполагают, что они будут реализованы как настоящие байтовые хранилища в кеше, не обновление RMW содержащего слова. (Таким образом, это сделало бы защиту ECC для кеша L1d более дорогой, поскольку вместо 32-битной требовалась бы гранулярность байтов).

Я предполагаю, что word-RMW во время фиксации в кеше L1d не рассматривался как вариант реализации для других более новых ISA, которые реализуют хранилища байтов.

Все современные архитектуры (кроме ранней Alpha) могут выполнять истинную загрузку / сохранение байтов в не кэшируемые области MMIO (не циклы RMW), что необходимо для записи драйверов устройств для устройств, которые имеют смежные байтовые регистры ввода / вывода. (например, с помощью внешних сигналов включения / выключения, чтобы указать, какие части более широкой шины содержат реальные данные, например, 2-битный TSIZ (размер передачи) на этом CPU / микроконтроллере ColdFire , или, например, один PCI / PCIe один передача байтов или подобные управляющие сигналы DDR SDRAM, которые маскируют выбранные байты.)

Может быть, создание цикла RMW в кеше для хранилищ байтов было бы чем-то, что следует учитывать при разработке микроконтроллера, хотя это не относится к высокопроизводительному суперскалярному конвейерному дизайну, предназначенному для серверов / рабочих станций SMP, таких как Alpha?

Я думаю, что это утверждение может исходить от машин, адресуемых по словам. Или из невыровненных 32-разрядных хранилищ, требующих множественного доступа ко многим ЦП, и людей, которые неправильно обобщают данные из хранилищ байтов.


Просто для ясности, я ожидаю, что цикл хранения байтов с тем же адресом будет выполняться с теми же циклами на итерации, что и цикл хранения слов. Таким образом, для заполнения массива 32-битные хранилища могут увеличиться в 4 раза быстрее, чем 8-битные. (Возможно, меньше, если 32-разрядные хранилища насыщают полосу пропускания памяти, а 8-разрядные хранилища - нет.) Но если у хранилищ байтов нет дополнительных штрафов, вы не получите больше , чем 4-кратная разница в скорости. (Или независимо от того, какое слово имеет ширина).

И я говорю об асме. Хороший компилятор будет автоматически векторизовать цикл хранения байтов или int в C и использовать более широкие хранилища или то, что оптимально для целевого ISA, если они смежные.

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

; 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 из хранилищ байтов, конкурирующих с пропускной способностью загрузки.

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


( Я не упомянул загрузку байтов, потому что это, как правило, просто : получить доступ к полному слову из кэша или ОЗУ, а затем извлечь нужный байт. Эти детали реализации неотличимы, кроме MMIO, где ЦП определенно не работаютне читайте содержащее слово.)

В архитектуре загрузки / хранения, такой как MIPS, работа с байтовыми данными просто означает, что выСе lb или lbu чтобы загрузить и обнулить или подписать его, затем сохраните его с sb.(Если вам нужно усечение до 8 бит между шагами в регистрах, тогда вам может потребоваться дополнительная инструкция, поэтому обычно локальные переменные должны иметь размер регистра. Если вы не хотите, чтобы компилятор автоматически векторизовал SIMD с 8-битными элементами, то часто uint8_tЛокальные данные хороши ...) Но в любом случае, если вы делаете это правильно, а ваш компилятор работает хорошо, не нужно никаких дополнительных инструкций для использования байтовых массивов.

Я заметил, что gcc имеет sizeof(uint_fast8_t) == 1 в ARM, AArch64, x86 и MIPS.Но ИДК, сколько акций мы можем положить в это.ABI System V x86-64 определяет uint_fast32_t как 64-битный тип на x86-64.Если они собираются это сделать (вместо 32-битного размера по умолчанию для x86-64), uint_fast8_t также должен быть 64-битным типом.Может быть, чтобы избежать нулевого расширения при использовании в качестве индекса массива?Если он был передан как функция arg в регистр, так как он может быть бесплатно расширен до нуля, если вам все равно придется загрузить его из памяти.

Ответы [ 2 ]

0 голосов
/ 17 января 2019

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 .

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

Они говорят, что если вы будете держать язык за зубами и сумеете получить достаточно небольших хранилищ, попадете в кеш достаточно быстро, они остановят процессор, пока не смогут его догнать.

В документе также описывается, что SRAM без ECC имеет ширину 32 бита, что означает, что это также верно при компиляции ядра без поддержки ECC. У меня нет доступа ни к сигналам для этого интерфейса памяти, ни к документации, поэтому я не могу сказать наверняка, но если он реализован как 32-битный интерфейс без управления байтовой дорожкой, то у вас возникает та же проблема, он может записать только 32-битный элемент к этой SRAM, а не к дробным частям, поэтому для замены 8 или 16 битов необходимо использовать RMW в недрах кэша.

Короткий ответ, почему бы не использовать более узкую память, - это размер чипа, а для ECC размер удваивается, поскольку существует ограничение на количество проверочных битов, которые можно использовать даже при уменьшении ширины (7 бит на каждые 8 ​​бит гораздо больше битов, чтобы сохранить, чем 7 бит на каждые 32). Чем уже память, тем больше у вас сигналов для маршрутизации и вы не можете упаковать память настолько плотно. Квартира против группы отдельных домов, чтобы вместить одинаковое количество людей. Дороги и тротуары к входной двери вместо прихожих.

И особенно с таким одноядерным процессором, если только вы не попытаетесь преднамеренно (что я и сделаю), вряд ли вы случайно столкнетесь с этим и зачем повышать стоимость продукта: возможно, этого не произойдет?

Обратите внимание, что даже с многоядерным процессором вы увидите память, созданную следующим образом.

EDIT.

Хорошо, дошли до теста.

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

Если бы у меня было половинное слово (strh), то неудивительно, что оно также переносит чтение-модификацию-запись, поскольку оперативная память имеет ширину 32 бита (плюс любые экси-биты, если есть)

0001000C   str                                                                      
00010007   str                                                                      
0002FFFD   strh                                                                     
0002FFFD   strh                                                                     
0002FFFD   strb                                                                     
0002FFFD   strb

нагрузки занимают столько же времени, сколько ширина sram считывается как целое и помещается на шину, процессор извлекает из этого интересующие байтовые дорожки, поэтому для этого не нужно тратить время / часы.

0 голосов
/ 17 января 2019

Мое предположение было неверным. Современные микроархитектуры x86 в этом смысле действительно отличаются от некоторых (большинства?) Других ISA.

На узкие кешированные хранилища может быть наложен штраф даже на высокопроизводительных процессорах, отличных от x86. Тем не менее, уменьшение площади кэша может сделать использование int8_t массивов оправданным. (И на некоторых ISA, таких как MIPS, не нужно масштабировать индекс для режима адресации).

Слияние / объединение в буфере хранения между байтами сохраняет инструкции в одном и том же слове, прежде чем фактическая фиксация в L1d также может уменьшить или снять штраф. (x86 иногда не может этого сделать, потому что модель сильной памяти требует, чтобы все хранилища фиксировались в программном порядке.)


В документации ARM для Cortex-A15 MPCore (с ~ 2012 г.) говорится, что она использует 32-разрядную гранулярность ECC в L1d и фактически выполняет слово-RMW для узких хранилищ для обновления данных.

Кэш данных L1 поддерживает необязательную однобитовую корректную и двухбитную логику исправления ошибок обнаружения как в тегах, так и в массивах данных. Гранулярность ECC для массива тегов - это тег для отдельной строки кэша, а гранулярность ECC для массива данных - это 32-разрядное слово.

Из-за гранулярности ECC в массиве данных запись в массив не может обновить часть 4-байтовой выровненной ячейки памяти, поскольку недостаточно информации для вычисления нового значения ECC. Это относится к любой инструкции сохранения, которая не записывает одну или несколько выровненных 4-байтовых областей памяти. В этом случае система памяти данных L1 считывает существующие данные в кеше, объединяет измененные байты и вычисляет ECC по объединенному значению. Система памяти L1 пытается объединить несколько хранилищ для удовлетворения выравнивание 4-байтовой гранулярности ECC и исключение требования чтения-изменения-записи.

(Когда они говорят «система памяти L1», я думаю, что они имеют в виду буфер хранения, если у вас есть смежные хранилища байтов, которые еще не зафиксировали в L1d.)

Обратите внимание, что RMW является атомарным и включает только изменяемую строку кэша, принадлежащую исключительно владельцу. Это деталь реализации, которая не влияет на модель памяти. Итак, мой вывод о Может ли современное оборудование x86 не хранить один байт в памяти? все еще (вероятно) правильно, что x86 может, и как и любой другой ISA, предоставляющий инструкции для хранения байтов.


Cortex-A15 MPCore - это трехпроцессорный исполняющий процессор, работающий не по порядку, так что это не минимальная мощность / простая конструкция ARM, но они решили потратить транзисторы на OoO exec, но не эффективный байт магазины.

Предположительно без необходимости поддерживать эффективные невыровненные хранилища (которые программное обеспечение x86 с большей вероятностью примет / использует), поскольку более медленные хранилища байтов считались оправданными для более высокой надежности ECC для L1d без чрезмерных издержек.

Cortex-A15, вероятно, не единственное и не самое последнее ядро ​​ARM, работающее таким образом.


Другие примеры (найдены @HadiBrais в комментариях):

  1. Альфа 21264 (см. Таблицу 8-1 главы 8 этого документа) имеет 8-байтовую гранулярность ECC для своего кэша L1d. Более узкие хранилища (включая 32-битные) приводят к RMW, когда они фиксируются в L1d, если они не объединяются в буфере хранилища первыми. Документ объясняет полную информацию о том, что L1d может делать за часы. И, в частности, документы о том, что буфер хранилища объединяет хранилища.

  2. PowerPC RS64-II и RS64-III (см. Раздел об ошибках в этом документе). Согласно этой абстрактной , L1 процессора RS / 6000 имеет 7 бит ECC для каждых 32 битов данных.

Альфа была агрессивно 64-битной с нуля, поэтому 8-байтовая гранулярность имеет некоторый смысл, особенно если стоимость RMW в основном может быть скрыта / поглощена буфером хранилища. (например, возможно, обычные узкие места были где-то еще для большей части кода на этом процессоре; его многопортовый кэш обычно мог обрабатывать 2 операции за такт.)

POWER / PowerPC64 вырос из 32-разрядного PowerPC и, вероятно, заботится о запуске 32-разрядного кода с 32-разрядными целыми числами и указателями. (Таким образом, больше шансов сделать несмежные 32-битные хранилища для структур данных, которые не могут быть объединены.) Поэтому 32-битная гранулярность ECC имеет большой смысл.

...