Память компьютера

Пора-бы уже поставить тут жирную точку и закрыть этот пост, но осталась ещё нераскрытая тема - это устройство кэш-памяти. Как и все/скоростные устройства, кэш глубоко зарыт внутри ЦП и как он функционирует на самом деле, наверняка знают только его разработчики. В манах вопрос освящается мутно и не хватает простых примеров, чтобы распутать сложные понятия - посмотрим на кэш с другой стороны..

Кэш-память процессора

Работа кэш основана на принципе локальности обращений. Это уникальный механизм, способный предсказывать запросы к памяти более чем в 90% случаях, и это при соотношение размеров 1:100000

(4Gb/32Kb)

. Как показала практика, в любой момент времени программы используют только часть своего кода, которая реализует одни и те-же операции с несколько отличающимися данными. Затем переход к другой области памяти, и всё идёт по-кругу. Такое положение объясняется наличием циклов в коде, что играет кэшу на-руку.

Ещё с рассвета архитектур, ЦП общается с памятью посредством параллельной шины. Остальные девайсы переходят уже на более высокоскоростной последовательный интерфейс

(PCIe, USB3)

, однако шина памяти остаётся прежней - так было и будет, пока инженеры не придумают что-то взамен динамической ОЗУ. Именно DRAM тормозит весь тех/процесс, т.к. её ядро не способно превысить частотный барьер в 200 MHz - на более высоких частотах конденсаторы ячеек просто не успевают разряжаться. Динамическая DRAM хоть и тормознутая - зато большая и дешёвая, это и удерживает её позиции на рынке.

Чтобы хоть как-то решить проблему DRAM, было решено добавить между процессором и ОЗУ небольшую прослойку в виде статической кэш-памяти SRAM, где запоминающим элементом служит уже не конденсатор, а триггер. Регистры самого процессора собираются на таких-же триггерах, поэтому скорость обмена ЦП с кэш-памятью имеет коэффициент 1:1.

----------------------------------------

В наше время, все операции с памятью выполняются только в пакетном режиме, процессор не может записать в ОЗУ байт или слово, он загребает за-раз сразу по 64-байта

(шина=8, и счётчик BL=8)

, которые отправляет в свой кэш -

'64-byte cache line'

. Именно такой размер кэш-строки присущ буквально всем современным процессорам.

Ясно, что в кэше хранится не одна строка, они собираются в блоки из N-строк - один такой блок называют банком кэш-памяти, или на их манер

'Way'

, каждая строка имеет свой индекс. Кол-во строк в одном наборе зависит от ёмкости кэш, и лежит в диапазоне от 32 до 8192. На рисунке ниже представлен банк из 64 кэш-строк. Если в одной строке 64-байта, а всего строк тоже 64, то такой кэш способен отобразить одну 4-Kбайтную страницу памяти

(64x64=4096)

:

Инженеры распилили общее пространство кэш на две части: в первой хранятся непосредственно кэш-строки, а во второй - их адреса. Адресное поле назвали тегом

'TAG'

, в которое сбрасывается не весь 32-битный адрес строки, а только старшая его часть. У каждой кэш-строки свой тег, разрядность тега зависит от размера и организации кэш - чем больше кэш, тем меньше битов в теге. То есть теговая память кэша(L1) будет размером больше, чем теговая память кэша(L2).

В глазах кэша, мир существует в виде страниц физической памяти. Когда кэш смотрит на 4Kb страницу, то по индексу[0] он отображает у себя её байты 0-63, по индексу[1] - байты 64-127 и т.д. Такой шаблон повторяется для каждой страницы, поэтому данные третьей строки на странице[0], будут отличаться от данных такой-же строки на странице[1]. Но постойте... А куда мы собираемся отображать страницу[1] - в кэш-же пока имеется только 1-Way для нулевой страницы?

Обстоятельства вынуждают нас добавить в структуру кэш ещё один Way, ..но раз-уж есть такая возможность, то добавим не один, а сразу 7-штук! Тогда получим заявленную производителями организацию

'8-way cache'

. Если в одном наборе(Way) мы могли отобразить одну страницу памяти, то теперь восемь - а это 32Kb памяти:

Посмотрим на характеристики процессоров Intel из отчёта CPU-Z...

Здесь видно, что каждое ядро процессора имеет свой кэш. Раздельные кэши для инструкций и данных имеет только L1. На всех уровнях L1-L2-L3 его организация отличается как по ёмкости, так и по кол-ву наборов(Way) в диапазоне 4..16. Вот тут-то и начинается самое интересное:

Name             Intel Core i5 3230M
Codename         Ivy Bridge  (2-ядра)
Technology       22 nm
L1 Data cache    2 x 32 Kb,  8-way set associative, 64-byte line size
L1 Inst cache    2 x 32 Kb,  8-way set associative, 64-byte line size
L2 cache         2 x 256 Kb, 8-way set associative, 64-byte line size
L3 cache         3 MBytes,  12-way set associative, 64-byte line size
--------------------------------
Name             Intel Core i7 6700K
Codename         Skylake      (4-ядра)
Technology       14 nm
L1 Data cache    4 x 32 Kb,  8-way set associative, 64-byte line size
L1 Inst cache    4 x 32 Kb,  8-way set associative, 64-byte line size
L2 cache         4 x 256 Kb, 4-way set associative, 64-byte line size
L3 cache         8 MBytes,  16-way set associative, 64-byte line size

Изменчивый характер структуры кэш, тянет за собой модернизацию всей

(ответственной за работу кэш)

аппаратной части процессора. В зависимости от ёмкости, наборы Way меняют свой облик, добавляя или уменьшая кол-во индексов в своей тушке. Если 32Kb кэш организован как 8-Way в каждом из-которых по 64 индекса, то общее кол-во 64-байтных строк в таком кэш будет равно: 64*8=512, а общая ёмкость:

512*64-байт=32Kb

. Организацию любого кэш можно вычислить по определённой формуле, посмотрим на неё:

32Kb L1-cache (8-way)------------------------
     32Kb / 64-byte = 512 всего кэш-линеек
      512 / 8-way   =  64 индекса в одном наборе Way

CPU address: Offset = 6-бит для выбора одного из 64-байтов в строке
             Index  = 6-бит для выбора одного из 64-индексов
             Tag    = 20-бит (старшая часть адреса)

256Kb L2-cache (4-way)-----------------------
    256Kb / 64-byte = 4096 cache line
     4096 / 4-way   = 1024 index

    Address: Offset = 6-bit
             Index  = 10-bit
             Tag    = 16-bit

8Mb L3-cache (16-way)------------------------
      8Mb / 64-byte = 131072 cache line
   131072 / 16-way  = 8192 index

    Address: Offset = 6-bit
             Index  = 13-bit
             Tag    = 13-bit

Рассмотрим детали реализации подробней...

Длинна кэш-строки постоянна и равна 64-байтам. Для выбора одного из байтов строки требуется 6-бит - это младшие биты адреса, который процессор выставил на шину. В то-же время разрядность поля индекса уже не так постоянна, и зависит от ёмкости кэш и наборов(Way) в нём. Оставшиеся старшие биты адреса отправляются прямиком в теговую память. Посмотрим на следующий рисунок, где для тега выделяется 20-бит

(типичный L1-кэш ёмкостью 32Kb)

..

20-битный тег подразумевает выравнивание страниц памяти на 4Kb границу, поскольку младшие 12-бит адреса логически сбрасываются в нуль. Чтобы скоростной кэш не превратился в тормознутый флоп, при помощи аппаратного бит-теста поиск осуществляется одновременно во-всех 512-ти тегах буквально за 1 такт. Только в случае попадания в тег, контроллёр снимает с адреса индексную часть и приступает к проверке всех кэш-строк в данном банке Way, иначе запрос перенаправляется кэшу сл.уровня.

Очередным кэшам приходится уже туговато, у них теги меньшей разрядности, а значит им предстоит иметь дело уже не с страницами, а с сегментами - соответственно перебирать приходится уже больше индексов в банке(Way). Например 16-битный тег кэша(L2) очищает младшие 16-бит адреса, что вынуждает кэш работать уже с блоками, размером 64Kb. Блоки 8-метрового кэша(L3) выравниваются вообще на пол-метра, поэтому их индексация занимает больше времени. Младшие 6-бит адреса в поиске не принимают участия - в случае кэш-попадания, по их содержимому выбирается конкретный байт из Cache-Line.

Передача запроса на уровень ниже

(L1->L2->L3->Mem)

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

'Look-Aside'

. Если выстрел попал в L1, то запросы к остальным снимаются - в этом случае говорят о кэш-попадании

(Cache-Hit)

. Если-же случился промах

(Cache-Miss)

, то запросы к L2-3 снимаются поочерёдно, пока запрос не достигнет памяти ОЗУ. Вариант хороший, только съедает много энергии. Альтернативой ему служит вариант

'Look-Through'

, когда обращения начинаются только после фиксации кэш-промахов. При этом теряется всего 1-такт, зато экономится энергия.

Оранизация кэш

В природе мирно сосуществуют 3 способа отображения памяти в кэш: кэш прямого отображения

'Direct-Mapped'

, полностью ассоциативный кэш

'Fully-associative'

, и их комбинация - наборно-ассоциативный кэш

'Set-associative'

. Сейчас на всех уровнях применяется только 'Set-Cache', поэтому не будем забивать себе мозги, а рассмотрим только его.

В архитектуре

'Set-associative'

, все строки каждого из банков(Way) делятся на несколько наборов(

Set

). Если в банке имеем 64-строки, то их можно условно поделить на 2-набора, по 32-строки в каждом:

Когда такой кэш отображает в себе память, он будет пытаться поместить первые 32-строки каждой из страниц, только в свой набор Set(0). Для следующих 32-х строк страницы-памяти, отводится уже место в наборе Set(1) и т.д. То-есть строка(20) любой из страниц памяти не может попасть в набор(1), а только в набор(0) - в этом и заключается ассоциативность. По мнению кэш-контроллёра, искать строки при такой организации проще, чем когда они переплетаются в непонятный клубок - ему виднее..

Однако своё мнение на этот счёт имеет не бритый

'Fully-associative cache'

, который беспорядочно кидает строки в любую/свободную область банков Way

(кто первый, того и тапки)

.

'Direct-Mapped'

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

'Set-associative cache'

впитал в себя всё/лучшее своих предшественников и сдавать позиции не намерен.

Политика замещения данных в кэш

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

'Modified cache-line'

, и бит(V) сигнализирующего, что кэш-строка занята и кому-то принадлежит

'Valid cache-line'

. Эти биты добавляются старшими в тегах кэш-строки.

Бит-модификации даёт контроллёру знать, нужно-ли перезаписывать в ОЗУ строку перед выгрузкой её из кэш-памяти. Такая дилема возникает у него только в двух случаях: (1)когда процессор встречает инструкцию очистки кэша с выгрузкой строк

WBINVD

-

Write-Back Invalidate cache

, (2)при обычных обстоятельствах для поддерки когерентности кэш с оперативной памятью

(остальные ситуации вытекают из этих/двух)

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

Размеры кэша и ОЗУ сильно отличаются, поэтому неизбежно наступает момент, когда придётся освободить одну из кэш-строк, чтобы загрузить на её место новую. Алгоритмы замещения стары как-мир и плавают на трёх китах: случайный

RANDOM

- орёл или решка,

FIFO

- становимся в очередь, и

LRU

- редко используемая строка. Первый неэффективен, зато прост в реализации. Второй - более надёжный, но оставляет желать лучшего. Третий - требовательный к ресурсам, зато наиболее результативный - его и применяют в современной кэш-памяти. Биты V/M были введены в тег специально для поддержки работы алгоритма LRU.

Поддержка когерентности

Бывают ситуации, когда программа работает с внешним устройством, и это устройство ждёт каких-либо данных от программы. Процессор грузит исполняемый код в свой кэш, подготавливает данные, после чего сообщает девайсу, что запрос готов. Устройство обращается к памяти и... данных там не находит. Почему? Всё просто - данные-то есть, только остались они в кэш-памяти, и ЦП не побеспокоился о своевременной выгрузки их в ОЗУ. Такая ситуация носит проблемный характер, а её решение назвали

"Поддержкой когерентности"

.

В идеальном случае, отмеченные битом(М) изменнёные кэш-строки нужно выгружать в ОЗУ сразу после их модификации. Тогда проблем с когерентностью не будет, зато возникнет другая проблема - жуткие тормоза, поскольку процесс требует записи в неуклюжую память. Если программе приспичит менять данные через короткие промежутки времени, тогда ЦП только и будет делать, что записывать данные в ОЗУ, а мы сможем высушить вёсла за это время. Значит такой подход для кэша не преемлен - решать проблемы когерентности выпало на плечи двух политик записи в ОЗУ:

Write-Through

- подразумевает

сквозную записью

модифицированных кэш-строк. С данной политикой солидарен кэш(L1). Как только строка получает бит(М), она сразу копируется в кэш(L2) - задумчивость тут не прощается. L1 снимает с себя обязанность по разбору грязных строк, и озадачивает этим кэши следующих уровней.

Write-Back

- альтернатива первой. Она придерживается правил

отложенной записи

(М)строк. Для их хранения, современный кэш-контроллёр имеет не один, а целых четыре "буфера отложеной записи", организованых по-принципу FIFO - первым пришёл, первым выйдешь. Когда при записи возникает попадание в кэш, эти буферы сразу обновляются. Контроллёр ждёт освобождения шины-памяти

(её могут занять устройства c DMA)

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

Ознакомится с текущими политиками можно запросив отчёт у

'PC-Wizard'

.

Нужно сказать, что ни каждый софт их показывает, но если кого заинтересует, то вот:

>> Процессор
      Type            : Intel Celeron D  336J, сокет 775
      Кодовое имя     : Prescott

  > Data Cache L1     : 16 Кб,  8-way set-associative, 64-byte line
      Режим записи    : Write-Through
      Number of Sets  : 32

  > Кэшe L2           : 256 Кб, 4-way set-associative, 64-byte line
      Режим записи    : Write-Back
      Number of Sets  : 512

Таким образом, буферы отложенной записи позволяют на некоторое время откладывать фактическую запись в основную память. В последних процессорах политики могут комбинироваться на-лету 'Write-Combining'. Запись буферизуется во-всех режимах работы ЦП, исключение составляет лишь запись в порты ввода/вывода устройств.

Некэшируемые регионы памяти

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

(например, инициализации)

с тем, чтобы из кэша не вытеснялись более полезные фрагменты кода. Когда процессор натыкается на некэшируемый регион памяти, все обращения R/W перенаправляются сразу на системную шину. Такой тип требуется для ввода-вывода посредством

MMIO (Memory Mapped I/O)

.

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

INVD

(очистка без сброса М-строк в память)

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

Регионы и методы кэширования зависят от возможностей конкретного ЦП. Как-правило программируются они через набор его регистров

'MTRRs - Memory Type Range Registers'

(регистры свойств областей памяти)

, или внутри таблицы

'PAT - Page Attribute Table'

(таблица атрибутов страниц памяти)

:

Буфер странслированных адресов TLB

Ещё одной разновидностью кэш-памяти является ассоциативный буфер

TLB - Translation Lookaside Buffer

. Этот кэш-буфер во многом похож на кэши(L1.2.3), за исключением одной поправки - в нём нет места для данных. Кэш предназначен для хранения только адресов последних затребованных страниц виртуальной памяти. Если транслятор страниц отключен в блоке MMU процессора, то TLB так-же отключается. Прозрачен он и в реальном режиме.

Имеются три раздельных буфера TLB для хранения адресов 4K,2M и 4M-байтных страниц соответственно. Средняя ёмкость - 64 индекса для 4-Кбайтных страниц, что позволяет мгновенно сформировать физический адрес только для 256Kb основной памяти

(64х4)

. При этом по статистике, вероятность кэш-попаданий в TLB составляет 98%, т.е. только в 2-х случаях из 100 будут промахи:

Сути в том, что на трансляцию адреса виртуальной страницы уходит много времени. Сначала нужно опросить регистр(CR3), чтобы определиться с базой каталога страниц(PDT); потом просканировать каталог страниц(PT); ..и наконец в таблице(РТ) найти запись(PTE). Такой караван проверок отрицательно сказывается на производительности системы, поэтому использовав адрес страницы один раз, процессор запоминает его в страничном кэше TLB.

В следующий раз, когда процессору потребуется страница, он опрашивает и таблицу и кэш одновременно, и если происходит кэш-попадание, то получаем прирост скорости за счёт обхода этапа трансляции - адрес уже готовый. Кому всё побарабану - так это 12-битному оффсету, которому в этом театре-действий выпало играть роль массовки - поле предназначено для выбора смещения внутри найденной/4-Кбайтной страницы памяти.

---------------------------------------------------

Задраиваем люки..

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

0. Google
1. Intel Software Manual volume 1,2,3
2. Встроеные средства WinXP
3. Программы сбора информации: PC-Wizard, Dr.Hardware, HWiNFO-32
4. НЕХ-редакторы: WinHEX, HxD, Hiew
5. Редакторы: Word, Exel, Photoshop, EditPlus
6. Горячительные напитки - по вкусу..