Увеличение быстродействия с помощью дополнительных устройств
Отступление про внешние устройства.
Прямой доступ к памяти
Многие устройства (например, жёсткие диски, сетевые карты и т. д.) обмениваются с компьютером большими объёмами данных.
- Пословное чтение/запись этих данных через MMIO-регистры крайне неэффективно
- обмен малыми объёмами
- всю работу делает процессор
- Отображение части памяти устройства на оперативную память само по себе тоже неэффективно
- либо это то же MMIO, только адреса разные
либо кто-то должен отвечать за заполнение этой области и актуальность находящихся там данных — опять процессор?
⇒ Пускай само устройство научится есть из тарелчитать из оперативной памяти и записывать в неё
Не понадобится массовый MMIO (регистры управления удобнее оставить)
- Обмен теперь выглядит так:
- Записываем адрес в регистр адреса
- Записываем размер в регистр размера
- Записываем команду (скажем, чтения) в регистр команд
- Устройство начинает складывать данные прямо в память в указанное место
- …складывает
- …складывает
- а программа тем временем работает
- Когда устройство всё сложит, возникнет прерывание типа «Окончание операции В/В»
Возможные проблемы:
- Устройств несколько, а шина данных одна
- Арбитраж (кому первому предоставить доступ к памяти)
- Многоканальность
- Устройство (например, сетевой интерфейс) готово присылать данные в течение некоторого макропромежутка времени, и дёргать процессор на каждую передачу не хочется
⇒ Bus mastering («захват шины»): устройство само может начать операцию В/В и инициировать прерывание, т. е. реализует часть ПДП контроллера
Википедия пишет, что современные архитектуры могут вообще не иметь DMA-контроллера
Область обмена такая большая, что непрерывного куска в памяти просто не найти
Вместо одной области устройству передаётся список, и оно должно уметь раскладывать данные во все элементы списка
🛞 Отступление про шины 🛞
- Исторически: пучок проводов к устройствам (название предположительно из немецкого)
Прерывания и MMIO: контроллер шины памяти
- Разделение шины данных и шины команд
- DMA, быстрые и медленные устройства: шина памяти отдельно, шины устройств отдельно, приоритизация и т. п.
- Bus mastering — управление шиной со стороны устройства (а не процессора)
- …
Кеш
Некоторые частые и медленные операции можно убыстрить за счёт аппаратного сбора и хранения дополнительной информации. Например, самые частые обращения в медленную память можно кешировать в быстрой памяти, а адрес следующей инструкции в условном переходе предсказывать (это также актуально при упреждающих вычислениях).
Кеширование как идея
Базовая статья: Что такое кэш процессора, и как он работает
Кеш — механизм аккумуляции части данных медленного устройства большого объёма с помощью быстрого устройства малого объёма.
Повышение быстродействия за счёт скорости обмена, т. к. большинство требуемых данных оказываются на быстром устройстве
- Повышение эффективности за счёт агрегации (укрупнения единиц обмена), предзагрузки и отложенной выгрузки данных
Пример: дисковый кеш в памяти
- Быстродействие памяти на несколько порядков выше быстродействия диска
Активным приложениям нужны не все файлы с диска, а только несколько (тысяч?) штук
Даже если диск очень быстрый (NVME), он всё равно оптимизирован для последовательного доступа
- Работа с кешем в памяти эффективнее работы с диском:
- Обмен с диском поблочный, с памятью — побайтный
- Если приложение начало читать из файла, оно скорее всего, будет читать и дальше из него — можно закешировать заранее
- Если приложение записывает в файл, возможно, оно и дальше туда писать, а если это MMAP-файл, то — в произвольное его место, и, возможно, неоднократно. Если немного подождать с выгрузкой «грязного» кеша, можно много сэкономить
Для повышения быстродействия в кеше должны содержаться как можно более «популярные» данные
Временная локальность: доступ к некоторому ограниченному фрагменту «большой» памяти востребован много раз в течение небольшого промежутка времени (когда выполняется работающий с ним фрагмент программы)
Адресная локальность: соседние ячейки памяти часто востребованы вместе (например, при работе с массивами)
Меньший размер и увеличение быстродействия иногда связаны напрямую:
- чтение из памяти: выставление адреса → доступ к ячейке по адресу → чтение данных → запись во внутренний блок
- чтение из регистра: непосредственная запись из регистра (определяется на стадии разбора инструкции) во внутренний блок
Методика заполнения и очистки кеша
Заполнение кеша
- По запросу: в кеш попадают только данные, к которым производился доступ
- Упреждающее чтение: в кеш также попадают данные, лежащие на кешируемом устройстве непосредственно после только что прочтённых
- Иногда получается автоматически (чтение блока с диска)
- Иногда можно сделать более эффективным (кеширование сразу файла или сразу многих команд до команды перехода)
- Чем больше упреждение, тем выше вероятность закешировать ненужное
- Анализ поведения программы (например, размер упреждения в зависимости от места в программе, кеширование данных, к которым она обратится, т. к. это записано в ещё не исполненном коде и т. п.)
Устаревание данных в кеше
- Случайные
- эффективная аппаратная реализация
- невысокая эффективность кеширования
- Редко используемые (LFU, less frequently used)
- требуется хранить счётчик использования
- требуется поиск самого низкого значения счётчика
плохо работает в чистом виде (только что добавленный элемент использовался один раз, но, согласно принципу временной локальности, именно его не надо тут же выбрасывать из кеша)
- Давно не используемые (LRU, less recently used)
- в чистом виде требуется хранить возраст
- Очередь (FIFO)
- в чистом виде — эффективная аппаратная реализация
- невысокая эффективность кеширования
- FIFO+LRU
самый свежий элемент всегда ставится в начало очереди (или переставляется в начало, если он был в ней)
- удаляется самый последний элемент (он и есть самый старый)
Замечание: кеш эффективен статистически: всегда можно создать алгоритм или наткнуться на класс задач, сильно понижающий актуальность кешируемых данных (например, пересылка огромных объёмов данных, расположенных в памяти в случайном порядке).
Терминология
Попадание (cache hit) — однократный доступ к закешированному элементу
Промах (cache miss) — однократный доступ к незакешированному элементу
Кеширование одного байта/ячейки невыгодно: слишком много метаинформации на один элемент кеша:
| 00 | 10 04 00 00 | a0 78 4f 95 |
- 8 битов — счётчик возраста
- 30 битов — адрес
- 32 бита — содержимое ячейки
Уже тут не нужно 32 битов для адреса, т. к. адрес ячейки всегда кратен 4
Разовьём идею. Пользуясь адресной локальностью, можно эффективно кешировать непрерывные области памяти:
| 00 | 10 04 00 | a0 78 4f 95 … 34 45|
- 8 битов — счётчик возраста
24 бита — тег адреса
8192 бита — содержимое ячеек (строка кеша)
Тег адреса в кеше — часть адреса, используемая для идентификации кешируемой области
Строка кеша — минимальная кешируемая область данных
Кеш прямого отображения
Принцип:
- Память разбивается на области, равные суммарному объёму кеша
- Каждая строка кеша хранит фиксированную строку одной из этих областей памяти
Размер тега зависит от количества областей, кешируемых одной и той же строкой.
- В примере таких области 4 (количество линий, исходящих из каждой строки), значит, тег займёт всего 2 бита.
Адрес внутри области в случае прямого отображения для каждой строки кеша задан заранее
Замечание: алгоритм «привязки» областей памяти к той или иной строке кеша может быть любым, но чаще всего используется тот, что в примере: память представляется в виде последовательно расположенных разделов (того же объёма, что и кеш), и тегом является номер раздела. Каждый раздел, в свою очередь, представлен в виде областей, соответствующих строкам кеша.
Как работает:
- При обращении к памяти через кеш вычисляются:
- тег адреса — целая часть деления адреса на размер кеша
номер строки — целая часть деления смещения относительно начала раздела на длину строки
- Если тег адреса совпадает с записанным в строку кеша, засчитывается попадание, и данные передаются из кеша
- Если тег адреса не совпадает с записанным в строку кеша, засчитывается промах, и происходит непосредственное обращение к памяти с обязательным заполнением строки
Согласно принципу адресной локальности такое разбиение наиболее эффективно: подряд идущие данные не вытесняют друг друга из кеша, если помещаются в нем
Прямой кеш имеет очень эффективную аппаратную реализацию, но не справляется с нарушениями принципа адресной локальности
Ассоциативный кеш
Любая строка может кешировать любую область памяти соответствующего размера (с выравниванием адреса).
Тег в ассоциативном кеше больше тега в прямом кеше:
- он также зависит от числа возможных кешируемых областей, а их столько, сколько строк умещается в памяти
- В примере строк 16, следовательно, размер тега — 4 бита
Как работает:
- При обращении к памяти через кеш вычисляется тег — целая часть деления адреса на размер строки
- Если строка с таким тегом содержится в кеше, засчитывается попадание, и данные передаются из кеша
- Если строки с таким тегом в кеше нет, засчитывается промах
- происходит непосредственное обращение к памяти
- выбирается строка-кандидат на вытеснение из кеша
- считанные данные помещаются в строку кеша
Ассоциативный кеш требует аппаратной реализации поиска тега, следовательно, его эффективность линейно падает с увеличением количества строк. Можно воспользоваться тем, что сравнения изначально независимы, и распараллелить поиск, снизим время до логарифма — параллельное сравнение, потом параллельное сравнение результатов и т. д. — но тогда линейно возрастёт количество ключей, а следовательно, и энергопотребление.
С другой стороны, ассоциативный кеш устойчив к нарушению принципа адресной локальности, а алгоритм вытеснения из кеша позволяет повысить эффективность и за счёт локальности временной.
Множественно-ассоциативный кеш
В современных многозадачных ОС с разделением времени принципы локальности несколько видоизменились
- Временная локальность либо обеспечена только на один квант работы процесса, либо захватывает данные всех активных процессов систем
- Адресная локальность демонстрирует не один очаг «горячих» данных, а несколько, по количеству активных процессов системы
Таким образом, востребована архитектура кеша
- большего по объёму
- поддерживающего более одного очага локальности
Естественное решение: разбить монолитный кеш прямого доступа на несколько «банков» (в соответствии с количеством ожидаемых очагов локальности), а банки объединить ассоциативно
В таком кеше
- размер тега равен размеру тега в одном банке прямого доступа (в примере — 1 бит)
- поиск тега среди строк разных банков сравнительно эффективен, т. к. линеен по количеству банков
- могут эффективно кешироваться несколько одновременно выполняющихся потоков команд и используемых ими областей данных
Мы с Википедией считаем, что множественно-ассоциативный кэш — это ассоциативно объединённые банки прямого доступа. Создатели RARS, похоже, сделали наоборот: при настройках Set size = 2 журнал «Data Cache Simulation» выглядит, как если бы сначала на основании тега выбирался банк («block range») из двух строк, а потом в банке производился ассоциативный поиск.
(8) address: 0x100080b0 (tag 0x00400202) block range: 6-7 trying block 6 tag 0x00400200 -- OCCUPIED trying block 7 empty -- MISS (9) address: 0x10008040 (tag 0x00400201) block range: 0-1 trying block 0 tag 0x00400200 -- OCCUPIED trying block 1 tag 0x00400202 -- OCCUPIED MISS due to FULL SET -- LRU replace block 0; unused since (1) (10) address: 0x10008000 (tag 0x00400200) block range: 0-1 trying block 0 tag 0x00400201 -- OCCUPIED trying block 1 tag 0x00400202 -- OCCUPIED MISS due to FULL SET -- LRU replace block 1; unused since (2) (11) address: 0x10008080 (tag 0x00400202) block range: 0-1 trying block 0 tag 0x00400201 -- OCCUPIED trying block 1 tag 0x00400200 -- OCCUPIED MISS due to FULL SET -- LRU replace block 0; unused since (9) (12) address: 0x10008010 (tag 0x00400200) block range: 2-3 trying block 2 tag 0x00400200 -- HIT
Кеш на запись
Всё это время разговор шёл о чтении из памяти
Кеширование записи очень заманчиво, т. к. согласно принципу локальности записей в память может быть много, и все они попадут в кеш, а синхронизацию с памятью можно сделать после или вообще по запросу на чтение.
Грязный кеш (dirty cache) — ситуация, когда содержимое кеша более новое, чем содержимое медленного устройство
Когерентность кеша — актуальное соответствие данных, находящихся в кеше, данным в памяти.
- В случае единственного кеша и доступа к памяти только посредством кеш-контроллера кеш будет всегда когерентен (даже грязный, ибо он актуальнее)
Некогерентное состояние кеша возникают в двух случаях:
- Наличие нескольких независимо обновляемых кешей на запись одного и того же места в памяти
Запись в память мимо кеша, после которой данные в памяти актуальны, а в кеше — нет
Стратегии кешировния с учётом записи:
- Запись не кешируется (сквозная запись, «wtite through»)
- если данные есть в кеше, их надо обновить
- если данных нет в кеше
- кеш изменяется, как если бы происходило чтение (заполнение по записи)
- заполнение по записи требует усложнения архитектуры, и не всегда эффективно
- ⇒ (вариант) кеш не изменяется
- кеш изменяется, как если бы происходило чтение (заполнение по записи)
Смысл: чтения этих данных могло ещё не производиться, но принцип локальности говорит, что они достаточно «горячие» (могут потребоваться)
- Запись кешируется (write back), т. е. вычисления продолжаются непосредственно после записи данных в кеш, без ожидания актуальной записи в память
- Для грязных строк нужно хранить дополнительный признак: «dirty» — строка содержит акутальные данные, не синхронизованные с памятью
- Чем дольше грязная строка закеширована, тем больше вероятность, что в неё ещё раз запишут ⇒ эффективность
- С другой стороны, чем больше грязных кешей, тем дороже их потеря (сброс питания, например) и тяжелее операция полной выгрузки
- Все операции с оперативной памятью (в частности, DMA) должны либо проходить через кеш (это неэффективно), либо отслеживаться кеш-контроллером
- ⇒ возникает дополнительный признак: «недостоверная» строка (invalid), означающий, что данные в строке неактуальны (например, память была обновлена по DMA)
- доступ на чтение в обход кеша также надо отслеживать и задерживать до синхронизации соответствующих грязных кешей
- Для грязных строк нужно хранить дополнительный признак: «dirty» — строка содержит акутальные данные, не синхронизованные с памятью
Всё это начинает крепко подгорать в многоядерных системах, которые имеют обыкновение
- Иметь отдельный кеш на каждом ядре
Лезть в память одновременно
(см. план лекции по многоядаерным системам)
В многопоточных системах (multi-HART) всё несколько проще, не ненамного.
Замечания и примеры
- Кеш данных и кеш инструкций удобно разделять:
- инструкции не кешируются на запись
- инструкции и данные могут образовывать два очага локальности
- для инструкций и для данных методы упреждения различны
- Кеш часто делают двухуровневым
- большой относительно медленный общий кеш L2 (скорее всего, прямого отображения + несколько ассоциативных банков)
- два маленьких совсем быстрых кеша (отдельно — инструкций L1I, отдельно — данных L1D; скорее всего, ассоциативные)
- В зависимости от архитектуры, L1 может быть подмножеством L2 или дополнением к нему. В последнем случае данные, вытесняемые из L1, помещаются в L2, а данные, закешированные в L1, вытесняются из L2 («остывание» / «разогрев»)
RARS
- Открыть в Rars «Tools → Data Cache Simulator»
- Открыть в Rars «Tools → Memory Reference Visualization»
- Изучить эффективность работы кешей (2×2=4 варианта)
- Организация: Direct Mapping (прямое отображение) / Fully Associative (ассоциативный)
- Вытеснение: LRU (давно не использованная строка) / Random (случайная строка)
Примеры:
- Гладкое чтение:
- Оба топологии— прямое отображение и ассоциатвная — одинаково эффективны
- Чтение вразбивку (с вытеснением):
- Кеш прямого отображения страдает от вытеснения (нарушается принцип локальности + коллизия тегов)
- Ассоциативный кеш эффективен
- Поскольку «горячих» зон всего две, эффективен также множественно-ассоциативный кеш на два банка (а вот было бы этих зон больше, чем банков — выгорел бы)
- Чтение с шагом
- Поэкспериментировать с размером шага: 8, 16, 20?
- Чтение с небольшим вытеснением:
- Поэкспериментировать с объёмом кешируемой памяти в том же примере: полтора размера кеша, два, полностью помещается в кеш
Предсказание перехода
Instruction Fetch — выборка инструкции из памяти
Instruction Decode — интерпретация инструкции
Execute — выполнение операции
Memory access — доступ к памяти
Write back — запись в регистровый блок (обновление регистров)
Условные переходы отрицательно влияют на эффективность выполнения программы: невозможно точно сказать, какую инструкцию следующей выбирать из памяти — следующую или по адресу перехода
в архитектурах с конвейером это приводит к появлению в конвейере «пузырька»-nopа прямо на этапе F (мы ещё не вычислили, какую именно, а выбирать вроде бы уже надо, пропускаем ход)
в архетиктурах с упреждающим выполнением это может привести к необходимости выполнять обе ветки вычислений (с двойным набором регистров и прочими сложностями), и стоимость вычисления «не той» ветки сильно возрастает
Можно попробовать пособирать статистику: в каких случаях условный переход был, а в каких — не было
Количественную (сколько раз был переход, сколько — не было)
- Количественную с устареванием
С учётом значений используемых регистров (например, ra, sp и *epc)
- ...
Для этого нужно предусмотреть специальную таблицу, в которой хранятся адреса инструкций (последних? наиболее популярных? см. политики кеширования) условных переходов и статистика по их использованию.
При выполнении такой инструкции процессор будет (в терминах RISC-V) заранее предсказывать адрес для стадии I, а в случае ошибочного предсказания оперативно заменять уже считанную инструкцию на «пузырёк»-nop (трюк со слотом задержки мы сейчас не рассматриваем).
RARS
В RARS реализована простейшая BHT (Branch History Table), собирающая статистику за один или два предыдущих вызова конкретного перехода. Даже в случае одного бита предсказания это должно повысить эффективность циклов (в которых множество переходов по одной ветке завершается единственным переходом по другой).
В случае двух битов предсказания можно ещё немного повысить эффективность классического цикла. Допустим, после серии переходов (цикл продолжался) произошёл не-переход (цикл закончился). Логично ожидать, что когда выполнение подойдёт к этому переходу в следующий раз, переход снова произойдёт (начнётся новый цикл), и однократной ошибки в предсказании недостаточно, чтобы «переменить мнение».
- Сделаем в третьем примере GAP побольше (скажем, 68), чтобы количество внутренних и внешних циклов было примерно одинаково.
Установим размер истории 1 и политику по умолчанию «NOT TAKE» (переход не произойдёт, если статистика отсутствует)
- Проверим
Установим размер истории 2 и политику по умолчанию «NOT TAKE» (переход не произойдёт, если статистика отсутствует или 1:1):
- Проверим
- Установим размер истории 2 и политику по умолчанию «TAKE»
- Проверим
Вариант «2/NOT TAKE»:
Вопрос: каков метод попадания инструкции перехода в BHT RARS?
Заметим, что стиль написания программы влияет на эффективность предсказания (как?)
Более глубокая статистика (например, 3), позволяет нивелировать эту зависимость эффективности от стиля, но вычислительно дороже.
В RARS таблица BHT содержи минимум 8 мест. Сколько условных переходов должно быть в программе, для которых эта таблица делается неэффективной по причине постоянных промахов?
Д/З
EJudge: VarCache 'Разные кеши'
Написать подпрограмму acache, которая вводит два числа: A (некоторый адрес в памяти), и T — тип эксплуатации кеша. Затем программа читает произвольные адреса в диапазоне A … A + 1024 (разумеется, не все) таким образом, чтобы:
При T < 0 самым эффективным алгоритмом кеширования оказался бы кеш прямого доступа (Direct Mapping) со стандартными настройками (LRU / 1 / 8 / 4 / 128)
При T > 0 самым эффективным алгоритмом кеширования оказался бы ассоциативный кеш (Fully Associative) с теми же настройками
При T == 0 самым эффективным алгоритмом кеширования оказался бы множественно-ассоциативный кеш (N-way Set Associative) с теми же настройками и количеством ассоциативных элементов в банке (Set size) 2
К программе будет приписан такой footer: VarCacheFooter.asm, а его вывод проверен программой моделирования кеша. Для проверки из RARS GUI вместо 0 в тесте можно использовать 268468224 (это $gp)
0 1
<ассоциативный кеш эффективнее>
Бонус. Написать программу, которая делает предсказание переходов (с конкретной настройкой и глубиной истории 2) анти-эффективной, т. е. существенно ниже 50%.