Този
раздел е нов.
Създаден е
през 2022 година.
6.3.2.1.1.
Изчистване
на кеш
паметите
За
сървърите от
клас Sandy Bridge-E, известни
след 2012 година,
които
споменахме по-горе,
йерархията
на паметта
може да бъде
илюстрирана
по следния
начин:
На
върха в
йерархията
са
изобразени
ядрата С1/Сn на
всеки от
процесорите.
Всяко
ядро има FIFO-буферна
памет за
изпреварващо
извлечените
и подготвени
за
изпълнение
машинни команди,
както и FIFO-буфер за
запис на
данни в
паметта. Тези
два буфера
захранват
ядрата с
команди и
данни. Под
тези два
структурни
елемента са
изобразени
трите нива на
кеш паметта
L1, L2 и L3.
Обърнете
внимание на
това, че L3 кешът е общ
за всички
ядра в един
процесор. Показан
е още броят
на тактовите
цикли за достъп
до
регистрите и
до клетките в
отделните
нива на кеш
паметта.
Връзката
на
процесорите
с
оперативната
памет се
осъществява
с помощта на
шина, наречена
Intel QuickPath Interconnect (QPI).
Това е
последователна
кеш-кохерентна
шина от тип
точка-точка.
Разработена
е от Intel
в отговор
на AMD
шината,
известна под
наименованието
Hyper Transport, описана
от нас в раздел
5.5.3.3 на тази
книга.
След
2008 г. шината QuickPath
заменя
по-рано
използваната
FSB (Front Side Bus), която
обслужва
трансфера на
данни между процесора
и северния
мост върху
дънните платки.
Всяка връзка
на шината QPI
включва
двойка
еднопосочни
канали, всеки
от които
физически е
реализиран
от 20 диференциални
двойки
проводници.
Данните се
предават във
вид на
пакети.
Пропускателната
способност
на един канал
е от 4,8 до 6,4[GT/s] (гигатранзакции).
Една
транзакция
съдържа 16
бита данни,
което прави 19,2[GiB/s] сумарна
пропускателна
скорост в
едно направление.
Диференциалната
връзка е
пояснена в раздел 5.5.3.1 на тази
книга (вижте фигура
5.5.3.1.10).
По-надолу
в йерархията
са показани
локалната
памет на
всеки
процесор (MC, Memory Core) и основната
DRAM памет с
възможните 4
канала.
Ще
разгледаме
отделните
нива в
йерархията
на паметта.
1. Регистри
Във
всяко ядро
има отделни
регистрови
набори за
съхранение
на цели числа
(160 на брой),
както и за
числа с
плаваща
запетая (144 на
брой). Тези
регистри са
достъпни в
рамките на
един цикъл и
представляват
най-бързата
памет,
налична в
процесорите.
Компилаторите
разпределят
локалните
променливи и
техните
функционални
аргументи в
тези
регистри.
Компилаторите
разпределят
подмножество
от регистри,
познати като
архитектурни
регистри,
след което
хардуерът ги
разширява,
докато
изпълнява
инструкциите
паралелно и
извън реда им
в програмния
текст.
Проблемите
на току що засегнатия
ред за
изпълнение
на машинните
команди са
подробно
обсъдени в глава 5 на тази
книга.
Компилаторите
са наясно със
способността
за
извънредно
(изпреварващо)
и паралелно изпълнение
за даден
процесор и
подреждат
потоци от
команди и
разпределянето
им върху
съответните
регистри.
Когато
хипернишката
е разрешена,
тези
регистри се
споделят
между
съвместно
разположените
хипернишки ;
2. Буфери за
подреждане (MOB – memory
ordering buffers)
MOB
съдържа 64
регистъра
за зареждане
(въвеждане) (load) и
36 регистъра
за запис (store).
Тези
буфери се
използват за
проследяване
на
операциите
по време на
прехвърляне,
докато чакат
в кеш
подсистемата,
тъй като инструкциите
се
изпълняват
извън реда.
Буферът за
запис
представлява
напълно
асоциативна
опашка, в
която може да
се търси за
съдържащи се
в опашката
команди за запис,
очакващи
обслужване
от кeш L1. Тези буфери
позволяват
на нашите
бързи процесори
да работят
без
блокиране,
докато данните
се
прехвърлят
към и от
подсистемата
на кеша.
Когато
процесорът
изпълнява
четене или запис,
те могат да
се върнат
извън реда. MOB
се използва
за
разграничаване
на
зареждането и
съхранението
при
постигане на
съответствие
с
публикувания
модел на
паметта. Машинните
команди за
четене и
запис могат
да се
изпълняват
извън реда на
зареждане в
буферите и
могат да се
върнат извън
реда в подсистемата
на кеша ;
3.
Кеш
памет от
първо ниво – L1
L1 е
локална кеш
памет на
ядрото, която
е разделена
на две
отделни
части - 32[KiB] кеш за данни
и 32[KiB] кеш за
команди. Времето
за достъп е 3
цикъла и може
да бъде скрито,
тъй като
командите се
предават в
ядрото като
данни, които
вече са в L1
кеша.
4.
Кеш
памет от
второ ниво – L2
Кешът L2
е локална
памет на
ядрото,
предназначена
да буферира
обмена между
L1 и общата (споделена)
L3 кеш. Кешът L2 е
с размер 256[KiB] и действа
като
ефективна
опашка за
достъп между
L1 и L3. L2 съдържа
както данни,
така и команди.
Латентността
за достъп до L2
е 12 цикъла.
5.
Кеш
памет от
трето ниво – L3
Кешът L3
се споделя
между всички
ядра в рамките
на един
сокет. L3 е
разделен на 2[MiB]-ви сегменти,
всеки
свързан към
мрежа с пръстеновидна
шина на
гнездото.
Всяко ядро
също е
свързано към
тази
пръстеновидна
шина. Адресите
се хешират
към сегменти
за по-голяма
пропускателна
способност.
Закъснението
може да бъде
до 38 цикъла в
зависимост
от размера на
кеша.
Размерът на
кеша може да
бъде до 20[MiB] в
зависимост
от броя на
сегментите,
като всеки
допълнителен
преход около
пръстена отнема
допълнителен
цикъл. Кешът L3
включва всички
данни на L1 и L2 за
всяко ядро на
един и същи
сокет. Това
включване, с
цената на
пространство,
позволява на
L3 кеша да прихваща
заявки, като
по този начин
премахва
тежестта от
частните
локални L1 и L2
кешове на
ядрото.
6.
Основна
(оперативна)
памет
DRAM
каналите са
свързани към
всеки сокет
със средна
латентност
от около 65[ns] за
локален
достъп на
сокета при
пълен пропуск
на кеша. Това
обаче е
изключително
променливо,
като е много
по-малко за
последващи достъпи
до колони в
същия буфер
на ред, до значително
повече,
когато
ефектите на
опашката и
циклите на
опресняване
на паметта са
в конфликт. 4
канала на
паметта са
агрегирани заедно
на всеки
сокет за
пропускателна
способност и
за скриване
на
латентността
чрез
конвейериране
на
независимите
канали на
паметта.
7.
NUMA (non-uniform
memory access) - неравномерен
достъп до
паметта
В
сървър с
няколко
гнезда имаме
нееднороден
достъп до
паметта. Той
е
нееднороден,
тъй като
необходимата
памет може да
е на отдалечен
сокет, който
изисква
допълнителни
40[ns] за скачване
през QPI шината. Sandy
Bridge е голяма стъпка
напред за
системи с 2
гнезда по
отношение на Westmere
и Nehalem. С Sandy Bridge
границата на
QPI е повишена
от 6.4[GT/s] на 8.0[GT/s] и две ленти
могат да
бъдат
обединени,
като по този
начин се
елиминира
тясното
място на
предишните
системи. В
допълнение, QPI
връзката
вече може да
препраща
заявки за
предварително
извличане,
които предишните
поколения не
са могли.
8.
Кратност
на
асоциативността
Кешовете
са ефективно
базирани на
хардуер хеш
таблици. Хеш
функцията
обикновено е
просто
маскиране на
някои битове
от нисък
порядък за
индексиране
на кеша. Хеш
таблиците се
нуждаят от
някои
средства за
справяне с
сблъсък за
същия слот.
Кратността
на
асоциативност
е броят на
слотовете,
които могат
да се
използват за
съхраняване
на
хешираната
версия на
адреса.
Наличието на
повече нива
на
асоциативност
е компромис
между
съхранението
на повече
данни спрямо
изискванията
за мощност и
време за търсене
по всеки от
начините. За Sandy
Bridge кешовете L1 и
L2 са с 8-кратна
асоциативност,
а L3 е с 12-кратна
асоциативност.
Архитектурното
изграждане
на
асоциативността
е подробно
изяснено в раздел 6.3.2 на
тази книга (вижте фигура
6.3.2.22).
Тъй
като някои
кешове са
локални за
ядрата, ние
се нуждаем от
средства за
поддържането
им
кохерентни,
така че
всички ядра
да имат
съвместим
(съответстващ)
вид спрямо
съдържанието
на основната
памет. Въпросът
с
кохерентността
на паметта е
подробно
изяснен в раздел
6.3.2.2 на тази
книга.
Подсистемата
на кеша се
счита за
"източник на
истината" за
масовите
системи. Ако
паметта се
извлича от
кеша, тя
никога не е
остаряла; кешът
е основното
копие, когато
данните съществуват
както в кеша,
така и в
основната памет.
Този стил на
управление
на паметта е
известен
като обратно
записване,
при което данните
в кеша се
записват
обратно в основната
памет само
когато
кеш-фреймът е
изхвърлен,
тъй като нов
фрейм заема
неговото място.
Кешът x86
работи върху
блокове
данни с размер
64 байта,
известни
като
кеш-линия.
Обръщаме
внимание на
факта, че
данновата
шина на
паметта е 64
битова. Други
процесори
могат да
използват
различен
размер за кеш-линията.
По-големият
размер на кеш
линията
намалява
ефективната
латентност
за сметка на
увеличените
изисквания
за честотна
лента.
За
да поддържа
кеша
кохерентни,
кеш контролерът
проследява
състоянието
на всеки кеш-ред
като намиращ
се в едно
състояние от краен
брой други
такива.
Протоколът,
който Intel
използва за
това, е MESIF, AMD
използва
вариант, известен
като MOESI.
Съгласно
протокола MESIF
всеки кеш-ред
може да бъде
в 1 от 5-те
следващи
състояния:
1. Modified (Променен):
Показва, че
кеш-редът е
замърсен и
трябва да
бъде записан
обратно в
паметта на
по-късен
етап. Когато
се запише
обратно в
основната
памет,
състоянието
преминава в
състояние Exclusive
(специално) ;
2. Exclusive (Специален):
Показва, че
кеш-линията
се държи
изключително
и че
съответства
на основната
памет. Когато
в нея се
запише ново
съдържание,
състоянието
ѝ отново преминава
в Modified. За
постигане на
това
състояние се
изпраща съобщението
Read-For-Ownership (RFO), което
включва
четене плюс
невалидно
излъчване
към всички
други копия;
3. Shared (Споделен):
Показва, че
съдържанието
на кеш-реда, е
чисто копие
на
съответния в
основната
памет фрейм ;
4. Invalid
(Невалиден):
Указва
неизползван
кеш-ред ;
5. Forward (Напред):
Показва
специализирана
версия на
споделеното
състояние,
т.е. това е определеният
кеш, който
трябва да
отговаря на други
кешове в NUMA
система.
За преминаване от едно състояние в друго се изпращат поредица от съобщения между кешовете. Преди Nehalem за Intel и Opteron за AMD, този трафик за кохерентност на кеша между сокетите трябваше да споделя шината на паметта, което значително ограничава мащабируемостта. Сега трафикът на контролера на паметта е на отделна шина. Шините QPI на Intel и HyperTransport на AMD се използват за поддържане на кохерентността на кеша между гнездата.
Кеш контролерът съществува като модул във всеки L3 кеш сегмент, който е свързан към мрежата с пръстеновидна шина на гнездото. Всяко ядро, както и L3 кеш сегментът, QPI контролерът, контролерът на паметта и интегрираната графична подсистема са свързани към тази пръстеновидна шина. Пръстенът се състои от 4 независими ленти за: заявки, подслушване, потвърждение и 32-байта даннова шина. Кешът L3 е включващ, тъй като всеки кеш-ред, съхраняван в L1 или L2 кеша, се съдържа, т.е. се съхранява в L3. Това осигурява бърза идентификация на ядрото, съдържащо модифицирана линия, когато се следи за промени. Кеш контролерът за L3 сегмент следи кое ядро може да има модифицирана версия на кеш-линия. Ако ядрото иска да прочете някаква памет, а нея я няма в Споделено, в Специално или в Променено състояние; след това трябва да направи отчитане на шината на пръстена. След това търсеното или ще бъде прочетено от основната памет, ако не е в подсистемите на кеша, или ще бъде прочетено от L3, ако е чист, или ще бъде подслушани от друго ядро, ако са модифицирани. Във всеки случай четенето никога няма да върне остаряло копие от подсистемата на кеша.
Ако нашите кешове винаги са последователни, тогава защо се тревожим за видимостта, когато пишем паралелни програми? Това е така, защото в рамките на нашите ядра, в стремежа им към все по-висока производителност, модификациите на данните могат да се появят извън ред за други нишки. Има 2 основни причини за това.
Първо, нашите компилатори могат да генерират програми, които съхраняват променливи в регистри за относително дълги периоди от време, поради съображения за производителност, например, променливи, използвани многократно в рамките на цикъл. Ако имаме нужда тези променливи да бъдат видими в ядрата, тогава актуализациите не трябва да бъдат разпределени в регистъра. Това се постига в езика C чрез квалифициране на променлива от тип "volatile"., т.е. стойността на променливата е изменяща се и следва да остава в паметта. Внимавайте, че в езиците C/C++ типът volatile е недостатъчен, за да се укаже на компилатора да не пренарежда други команди. За това имате нужда от подобни "огради/бариери" за памет.
Вторият основен проблем с подреждането, за който трябва да сме наясно, е, че нишката може да запише променлива и после, ако я прочете малко след това, може да види стойността ѝ в своя буфер за запис, която стойност може да е по-стара от най-новата стойност в под-кешовата система. Това никога не е проблем за алгоритми, следващи принципа на “Single Writer - Единичен записвач (SW)". Нека поясним:
Когато се опитвате да изградите силно мащабируема система, най-голямото ограничение за това е, че множество Writers се борят за всеки елемент от данни или ресурс. Разбира се, алгоритмите могат да бъдат лоши, но нека приемем, че имат разумна нотация за BigO. Много хора просто приемат наличието на множество Writers като норма. Има много изследвания в компютърните науки за управление на това твърдение, което се свежда до 2 основни подхода.
· Единият е да се осигури взаимно изключване на претендирания ресурс, докато се извършва мутацията ;
· Другият е да се предприеме оптимистична стратегия за размяна на промените, ако основният ресурс не се е променил, докато сте създали новото копие.
Ако имате
система,
която може да
спазва
принципа на SW,
тогава всеки
контекст на
изпълнение
може да
прекара
цялото си
време и
ресурси в
обработка на
логиката за
нейната
същностна цел,
а не да губи
цикли и
ресурси за
преодоляване
на проблема
със спора.
Можете също
така да увеличавате
Writers без
ограничение,
докато
хардуерът не
се насити.
Предимство е
и това, че
когато се
работи върху
архитектури,
като ix86/ix64,
където на
хардуерно
ниво има
модел на паметта,
при който
операциите Load/Store на
паметта
запазват
реда. Тогава
не са
необходими
бариери в
паметта,
стига да се
придържате
строго към
принципа SW.
За
по-задълбочено
изучаване на
този проблем
препоръчваме
на Читателя
да се отнесе
към
специализираната
литература.
Вторият основен проблем с подреждането, за който споменахме по-горе, е, че нишката може да запише променлива и после, ако я прочете малко след това, може да види стойността ѝ в своя буфер за запис, която стойност може да е по-стара от най-новата стойност в под-кешовата система. Това никога не е проблем за алгоритми, следващи принципа на “Single Writer - Единичен записвач (SW)".
Буферът за запис (Store buffer) също така позволява на командата за зареждане (Load) да изпревари по-стар запис, създавайки по този начин проблем на подобни алгоритми за заключване, като тези на Dekker и Peterson. За да се преодолеят тези проблеми, нишката не трябва да позволява на последователно зареждане да изпревари последователното съхранение на стойността в локалния буфера за запис. Това може да се постигне чрез издаване на команда за ограждане (укриване). Записването на изменяща се променлива в Java, освен че никога не се разпределя в регистъра, е придружено от команда за ограждане. Командата за укриване в ix86 има значително въздействие върху производителността, като предотвратява напредъка на издаващата нишка, докато буферът на магазина не се източи. Оградите в други процесори могат да имат по-ефективни реализации, които просто поставят маркер в буфера за запис като границата за търсене.
Ако искате да осигурите подреждане на паметта в нишките на Java, когато следвате принципа на единичния запис (SW), и да избегнете оградата на буфера, можете да направите това с помощта на метода jucAtomic(Int|Long|Reference).lazySet(), вместо да задавате изменяща се променлива.
Връщайки се към заблудата за "прочистване на кеша", която споменахме в началото като част от паралелен алгоритъм. Можем спокойно да кажем, че никога не “промиваме“ кеша на процесора в рамките на нашите потребителски програми. Източникът на тази заблуда е необходимостта от промиване, маркиране или източване на буфера за запис до определена точка за някои класове паралелни алгоритми, така че най-новата стойност да може да се наблюдава при последваща операция за зареждане. За това ни е необходима ограда (укриване) за подреждане на паметта, а не изчистване на кеша.
Друг
възможен
източник на
тази заблуда
е, че
кешовете L1 или TLB може
да се наложи
да бъдат
прочистени въз
основа на
политика за
индексиране
на адреси при
превключване
на контекст. ARM,
преди ARMv6, не
използва
етикети на
адресното
пространство
за TLB записи,
което
изисква
целият кеш L1
да бъде
прочистен
при
превключване
на контекст.
Много
процесори
изискват
кешът на
командите L1
да бъде
прочистен по
подобни
причини. В
много случаи
това е просто
защото не се
изисква
кешовете на
командите да
се поддържат
последователни.
Изводът е, че
превключването
на контекста
е скъпо и
малко извън
темата, така
че в допълнение
към
замърсяването
на L2 кеша на,
превключването
на контекста
може също да
накара TLB и/или L1
кешовете да
изискват
прочистване.
Процесорите
Intel x86 изискват
само TLB flush при
превключвател
на контекста.
Следващият
раздел е:
6.3.2.1.2.
Етапи в
протокола за
обслужване
на
първичната кеш
памет