Барьер памяти
Барьер памяти (англ. memory barrier, membar, memory fence, fence instruction) — вид барьерной инструкции, которая приказывает компилятору (при генерации инструкций) и центральному процессору (при исполнении инструкций) устанавливать строгую последовательность между обращениями к памяти до и после барьера. Это означает, что все обращения к памяти перед барьером будут гарантированно выполнены до первого обращения к памяти после барьера.
Барьеры памяти необходимы, так как большинство современных процессоров используют оптимизации производительности, которые могут привести к переупорядочиванию инструкций. Также переупорядочивание обращений к памяти может быть вызвано компилятором в процессе оптимизации использования регистров целевого процессора. Такие перестановки обычно не влияют на корректность программы с одним потоком исполнения, но могут вызвать непредсказуемое поведение в многопоточных программах. Правила изменения порядка исполнения инструкций зависят от архитектуры. Некоторые архитектуры предоставляют несколько типов барьеров с различными гарантиями. Например, amd64 предоставляет следующие инструкции: SFENCE
(англ. store fence), LFENCE
(англ. load fence), MFENCE
(англ. memory fence)[1].
Intel Itanium обеспечивает отдельные «запоминающие» (англ. acquire) и «освобождающие» (англ. release) барьеры памяти, которые учитывают видимость операций чтения после записи с точки зрения читателя и писателя соответственно.
Барьеры памяти, как правило, используются при реализации примитивов синхронизации, неблокирующих структур данных и драйверов, которые взаимодействуют с аппаратным обеспечением.
Пример
Следующая программа исполняется на двух процессорах.
Изначально ячейки памяти x
и f
содержат значение 0
. Программа в процессоре #1 находится в цикле пока f
равен нулю, затем она печатает значение x
. Программа в процессоре #2 записывает значение 42
в x
, а затем сохраняет значение 1
в f
. Псевдокод для двух программных фрагментов:
Процессор #1:
while (f == 0) { }
// Здесь необходим барьер
print x;
Процессор #2:
x = 42;
// Здесь необходим барьер
f = 1;
Хотя ожидается, что print
всегда напечатает «42», но если процессор #2 изменит порядок исполнения инструкций и вначале изменит значение f
то print может вывести «0». Аналогично, процессор #1 может прочитать x
перед f
, и print снова выведет неожидаемое значение. Для большинства программ, ни одна из этих ситуаций не приемлема. Барьер памяти для процессора #2 может быть вставлен перед изменением значения f
. Также можно вставить барьер для процессора #1 перед чтением x
[2].
Оптимизации порядка исполнения компилятором
Барьеры памяти работают только на аппаратном уровне. Компиляторы могут также переупорядочить инструкции как часть оптимизации программы. Меры по предотвращению переупорядочивания необходимы только для данных, которые не защищены примитивами синхронизации.
В языках С и С++ ключевое слово volatile предназначено для исключения оптимизаций компилятора. Используется чаще всего для работы с отображаемым в память вводом-выводом. Однако данное ключевое слово (в отличие от Java) никак не обеспечивает атомарность и защиту от внеочередного исполнения.[3]
Примечания
- peeterjoot. Intel memory ordering, fence instructions, and atomic operations (4.9.2009).
- Другие примеры — в статье о блокировке с двойной проверкой
- Volatile Considered Harmful — Linux Kernel Documentation