Гарантирует ли барьер памяти, что согласованность кэша была завершена?
Скажем, у меня есть два потока, которые манипулируют глобальной переменной x
, Каждый поток (или каждое ядро, я полагаю) будет иметь кэшированную копию x
,
Теперь скажи, что Thread A
выполняет следующие инструкции:
set x to 5
some other instruction
Теперь когда set x to 5
выполняется кэшированное значение x
будет установлен в 5
это приведет к тому, что протокол когерентности кеша будет действовать и обновит кеши других ядер новым значением x
,
Теперь мой вопрос: когда x
на самом деле установлен на 5
в Thread A
кеш, обновляются ли кэши других ядер раньше some other instruction
выполняется? Или барьер памяти должен быть использован для обеспечения этого?
set x to 5
memory barrier
some other instruction
Примечание. Предположим, что инструкции были выполнены по порядку. set x to 5
выполняется,5
немедленно помещается в кэш Thread A` (поэтому инструкция не была помещена в очередь или что-то, что будет выполнено позже).
3 ответа
Барьеры памяти, присутствующие в архитектуре x86 - но это в целом верно - не только гарантируют, что все предыдущие1 загрузки или хранилища завершены до выполнения любой последующей загрузки или сохранения - они также гарантируют, что хранилища стали глобально видимыми,
Под глобально видимым подразумевается, что другие агенты, поддерживающие кеширование, такие как другие процессоры, могут видеть хранилище.
Другие агенты, не знающие о кешах - например, устройства с поддержкой DMA - обычно не видят хранилище, если целевая память была помечена типом кеша, который не требует немедленной записи в память.
Это не имеет никакого отношения к самому барьеру, это простой факт архитектуры x86: кеши видны программисту, а при работе с оборудованием они обычно отключены.
Intel намеренно обобщает описание барьеров, потому что она не хочет привязывать себя к конкретной реализации.
Вы должны мыслить абстрактно: глобально видимый означает, что оборудование предпримет все необходимые шаги, чтобы сделать магазин глобально видимым. Период.
Однако, чтобы понять барьеры, стоит взглянуть на текущие реализации.
Обратите внимание, что Intel может по своему усмотрению перевернуть современную реализацию вверх дном, если она сохраняет правильное поведение.
Хранилище в процессоре x86 выполняется в ядре, затем помещается в буфер хранилища.
Например mov DWORD [eax+ebx*2+4], ecx
После того, как расшифрованный остановлен до eax
, ebx
а также ecx
готовы2, затем он отправляется исполнительному блоку, способному вычислить его адрес.
Когда выполнение завершено, хранилище становится парой (адрес, значение), которая перемещается в буфер хранилища.
Говорят, что магазин будет завершен локально (в основном).
Буфер хранилища позволяет OoO-части ЦП забыть о хранилище и считать его завершенным, даже если попытка записи еще не была предпринята.
При определенных событиях, таких как событие сериализации, исключение, выполнение барьера или исчерпание буфера, CPU очищает буфер хранилища.
Флеш всегда в порядке - первый вошел, первый написан.
Из буфера хранилища хранилище входит в область кэша.
Он может быть объединен еще в другой буфер, называемый буфером объединения записей (и позже записан в память путем обхода кэшей), если целевой адрес помечен типом кэша WC, он может быть записан в кэш L1D, L2, L3 или LLC, если он не является одним из предыдущих, если тип кэша - WB или WT.
Он также может быть записан непосредственно в память, если тип кэша - UC или WT.
Как сегодня, это то, что значит стать глобально видимым: оставить буфер хранилища.
Остерегайтесь двух очень важных вещей:
- Тип кэша все еще влияет на видимость.
Видимый глобально не означает видимый в памяти, он означает видимый, где его увидят нагрузки от других ядер.
Если область памяти является кешируемой в ББ, загрузка может закончиться в кеше, поэтому она видна там глобально - только для агента, знающего о существовании кеша. (Но обратите внимание, что большая часть DMA на современных x86 является кэш-когерентной). - Это также относится к некогерентному буферу WC.
WC не поддерживается согласованным - его цель - объединить хранилища с областями памяти, где порядок не имеет значения, например с кадровым буфером. Это пока не видно глобально, только после очистки буфера объединения записи все, что находится за пределами ядра, сможет его увидеть.
sfence
делает именно это: ждет, пока все предыдущие хранилища завершатся локально, а затем истощает буфер хранилища.
Поскольку каждое хранилище в буфере хранилища может потенциально отсутствовать, вы видите, насколько тяжелой является такая инструкция. (Но выполнение не по порядку, включая последующие загрузки, может продолжаться. Только mfence
будет блокировать последующие загрузки от общего доступа (чтение из кэша L1d) до тех пор, пока буфер хранилища не завершит фиксацию в кэше.)
Но делает sfence
ждать, пока магазин распространится в другие тайники?
Ну нет.
Поскольку нет распространения - давайте посмотрим, что подразумевает запись в кеш с точки зрения высокого уровня.
Кэш-память сохраняется согласованной среди всех процессоров с протоколом MESI (MESIF для систем с несколькими сокетами Intel, MOESI для систем AMD).
Мы увидим только MESI.
Предположим, что write индексирует строку L кэша, и предположим, что все процессоры имеют эту строку L в своих кэшах с одинаковым значением.
Состояние этой линии является общим в каждом процессоре.
Когда наши хранилища попадают в кэш, L помечается как измененный, и на внутренней шине выполняется специальная транзакция (или QPI для систем с несколькими сокетами Intel) для аннулирования строки L в других процессорах.
Если L изначально не находился в состоянии S, протокол изменяется соответствующим образом (например, если L находится в состоянии Exclusive, никакие транзакции на шине не выполняются[ 1]).
На этом этапе запись завершена и sfence
завершается.
Этого достаточно, чтобы сохранить связность кэша.
Когда другая строка запроса ЦП L, наш ЦП отслеживает этот запрос, и L сбрасывается в память или во внутреннюю шину, чтобы другой ЦП прочитал обновленную версию.
Состояние L снова устанавливается на S.
Таким образом, в основном L читается по требованию - это имеет смысл, так как распространение записи на другой процессор стоит дорого, и некоторые архитектуры делают это, записывая L обратно в память (это работает, потому что другой процессор имеет L в состоянии Invalid, поэтому он должен прочитать его из объем памяти).
Наконец это не правда, что sfence
Обычно все они бесполезны, наоборот, они чрезвычайно полезны.
Просто обычно нас не волнует, как другие процессоры видят, как мы создаем наши хранилища, но получение блокировки без получения семантики, как определено, например, в C++, и реализовано с помощью заборов, совершенно чокнуто.
Вы должны думать о барьерах, как говорит Intel: они обеспечивают порядок глобальной видимости обращений к памяти.
Вы можете помочь себе понять это, думая о барьерах как об обеспечении порядка или записи в кеш. Когерентность кеша тогда будет гарантировать, что запись в кеш видна глобально.
Я не могу не подчеркнуть еще раз, что когерентность кэша, глобальная видимость и упорядочение памяти - это три разные концепции.
Первое гарантирует второе, что обеспечивается третьим.
Memory ordering -- enforces --> Global visibility -- needs -> Cache coherency
'.______________________________'_____________.' '
Architectural ' '
'._______________________________________.'
micro-architectural
Примечания:
- В программном порядке.
Это было упрощение. На процессорах Intel,
mov [eax+ebx*2+4], ecx
декодирует в два отдельных мопа: store-address и store-data. Адрес магазина UOP должен ждать, покаeax
а такжеebx
готовы, затем он отправляется исполнительному блоку, способному вычислить его адрес. Этот исполнительный модуль записывает адрес в буфер хранилища, поэтому последующие загрузки (в порядке программы) могут проверить пересылку хранилища.когда
ecx
готово, хранилище данных может отправлять данные на порт хранилища данных и записывать данные в ту же запись буфера хранилища.Это может произойти до или после того, как адрес известен, потому что запись буфера хранилища зарезервирована, вероятно, в программном порядке, поэтому буфер хранилища (так называемый буфер порядка памяти) может отслеживать порядок загрузки / сохранения, как только адрес всего будет известен и проверьте наличие совпадений. (И для спекулятивных нагрузок, которые заканчивались нарушением правил упорядочения памяти в x86, если другое ядро сделало недействительной строку кэша, загруженную им до самой ранней точки, которую им было разрешено архитектурно разрешить. Это приводит к очистке конвейера ошибочных спекуляций порядка памяти.)
Теперь, когда выполняется установка x в 5, кэшированное значение x будет установлено в 5, это заставит протокол когерентности кэша действовать и обновит кэши других ядер новым значением x.
Существует несколько разных процессоров x86 с разными протоколами когерентности кэша (нет, MESI, MOESI), а также различные типы кэширования (без кэширования, комбинирование записи, только запись, сквозная запись, обратная запись).
В общем, когда выполняется запись (при установке x на 5), CPU определяет тип выполняемого кэширования (из MTRR или TLB), и, если строка кэша может быть кэширована, он проверяет свой собственный кэш, чтобы определить, в каком состоянии этот кэш линия находится в (с его собственной точки зрения).
Затем тип кэширования и состояние строки кэша используются для определения того, записываются ли данные непосредственно в физическое адресное пространство (в обход кэшей), или нужно ли извлекать строку кэша из другого места, одновременно сообщая другим процессорам о необходимости аннулировать старые копии, или если он имеет эксклюзивный доступ в своих собственных кешах и может изменить его в кеше, ничего не сказав.
ЦП никогда не "внедряет" данные в кеш другого ЦП (и только говорит другим ЦПУ аннулировать / удалить их копию строки кеша). Указание другим процессорам аннулировать / отбросить их копию строки кэша заставляет их выбирать текущую копию, если / когда они хотят ее снова.
Обратите внимание, что все это никак не связано с барьерами памяти.
Есть 3 типа барьеров памяти (sfence
, lfence
а также mfence
), который указывает ЦПУ завершить сохранение, загрузку или и то, и другое, прежде чем разрешить последующие сохранения, загрузку или то и другое одновременно. Поскольку процессор обычно в любом случае согласован с кэшем, эти барьеры / ограждения памяти обычно не имеют смысла / не нужны. Однако существуют ситуации, когда ЦП не является когерентным кешу (включая "пересылку хранилища", когда используется тип кэширования с комбинированием записи, когда используются не временные хранилища и т. Д.). Барьеры / ограждения памяти необходимы для обеспечения порядка (при необходимости) в этих особых / редких случаях.
Нет, барьер памяти абсолютно не гарантирует, что согласованность кэша была "завершена". Часто это вообще не предполагает операции согласования и может быть выполнено спекулятивно или в качестве запрета.
Он только обеспечивает семантику упорядочения, описанную в барьере. Например, реализация может просто поместить маркер в очередь хранилища так, чтобы пересылка из хранилища в загрузку не происходила для хранилищ старше маркера.
В частности, у Intel уже есть сильная модель памяти для обычных нагрузок и хранилищ (типа, который генерируют компиляторы и который вы будете использовать при сборке), где единственно возможный переупорядочение - это более поздние загрузки, проходящие через более ранние хранилища. В терминологии барьеров памяти SPARC, каждый барьер, кроме StoreLoad
уже нет
На практике интересные барьеры на x86 прикреплены к LOCKed
инструкции, и выполнение такой инструкции не обязательно вообще подразумевает какую-либо когерентность кэша. Если строка уже находится в исключительном состоянии, ЦП может просто выполнить инструкцию, следя за тем, чтобы не освобождать исключительное состояние строки во время выполнения операции (т. Е. Между чтением аргумента и обратной записью результата) а затем заниматься только предотвращением пересылки из магазина в загрузку, чтобы нарушить общий порядок LOCK
инструкции идут с. В настоящее время они делают это, опустошая очередь магазина, но в будущих процессорах даже это может быть спекулятивным.
Что делает барьер памяти или барьер + операция, так это то, что другие агенты видят операцию в относительном порядке, который подчиняется всем ограничениям барьера. Это, конечно, обычно не подразумевает передачу результата другим процессорам в качестве операции согласования, как предполагает ваш вопрос.
Если ни один из других процессоров не имеет X в своем кэше, выполнение x=5 на процессоре A не приведет к обновлению кешей в любом другом процессоре. Если процессор B читает переменную X, процессор A обнаружит чтение (это называется отслеживанием) и предоставит данные 5 на шину для процессора B. Теперь процессор B будет иметь значение 5 в своем кэше. Если никакой другой процессор не считывает переменную X, их кеши никогда не будут обновлены с новым значением 5.