Атомность на x86
8.1.2 Блокировка шины
Процессоры Intel 64 и IA-32 выдают сигнал LOCK#, который автоматически устанавливается во время определенных критических операций с памятью для блокировки системной шины или эквивалентного канала. Пока этот выходной сигнал подается, запросы от других процессоров или агентов шины на управление шиной блокируются. Программное обеспечение может указывать другие случаи, когда семантике LOCK нужно следовать, добавляя префикс LOCK к инструкции.
Он взят из Руководства Intel, Том 3
Похоже, что атомарные операции над памятью будут выполняться непосредственно в памяти (RAM). Я запутался, потому что не вижу ничего особенного, когда анализирую результаты сборки. В основном, вывод сборки, созданный для std::atomic<int> X; X.load()
ставит только "лишнюю" защиту. Но он отвечает за правильное упорядочение памяти, а не за атомарность. Если я правильно понимаю X.store(2)
просто mov [somewhere], $2
, И это все. Кажется, он не "пропускает" кеш. Я знаю, что перемещение выровненных (например, целых) в память атомарно. Однако я в замешательстве.
Итак, я высказал свои сомнения, но главный вопрос:
Как процессор выполняет атомарные операции внутри?
2 ответа
Похоже, что атомарные операции над памятью будут выполняться непосредственно в памяти (RAM).
Нет, пока каждый возможный наблюдатель в системе видит операцию как атомарную, операция может включать только кэш. Удовлетворение этого требования намного сложнее для атомарных операций чтения-изменения-записи (например, lock add [mem], eax
особенно с невыровненным адресом), когда процессор может выдавать сигнал LOCK#. Вы все равно не увидите больше ничего в ассемблере: аппаратное обеспечение реализует семантику, требуемую ISA для lock
Инструкции ред.
Хотя я сомневаюсь, что на современных процессорах есть физический внешний контакт LOCK#, в котором контроллер памяти встроен в процессор, а не в отдельный чип северного моста.
std::atomic<int> X; X.load()
ставит только "лишнюю" защиту.
Компиляторы не MFENCE для загрузки seq_cst. Я думаю, что прочитал, что MSVC испустил MFENCE для этого (возможно, чтобы предотвратить переупорядочение с неизолированными хранилищами NT?), Но это не так: я только что протестировал MSVC 19.00.23026.0. Ищите foo и bar в выводе asm этой программы, которая выводит свой собственный asm на онлайн-сайт компиляции и запуска.
Я думаю, что причина, по которой нам здесь не нужен забор, заключается в том, что модель памяти x86 не допускает переупорядочения LoadStore и LoadLoad. Более ранние (не seq_cst) хранилища все еще могут быть отложены до окончания загрузки seq_cst, поэтому они отличаются от использования автономныхstd::atomic_thread_fence(mo_seq_cst);
передX.load(mo_acquire);
Если я правильно понимаю
X.store(2)
простоmov [somewhere], 2
Нет, хранилищам seq_cst требуется инструкция полного барьера памяти, чтобы запретить переупорядочение StoreLoad, которое могло бы произойти в противном случае.
MSVC Asm для магазинов такой же, как Clang, используя xchg
сделать магазин и барьер памяти с той же инструкцией. (На некоторых процессорах, особенно AMD, lock
Инструкция ed в качестве барьера может быть дешевле, чем MFENCE, поскольку IIRC AMD документирует дополнительную семантику сериализации-конвейера (для выполнения инструкций, а не только упорядочения памяти) для MFENCE).
Этот вопрос выглядит как вторая часть вашей предыдущей модели памяти в C++: последовательная согласованность и атомарность, где вы задали вопрос:
Как процессор выполняет атомарные операции внутри?
Как вы указали в этом вопросе, атомарность не связана с упорядочением в отношении любых других операций. (т.е. memory_order_relaxed
). Это просто означает, что операция происходит как единая неделимая операция, отсюда и название, а не как несколько частей, которые могут происходить частично до и частично после чего-то другого.
Вы получаете атомарность "бесплатно" без дополнительных аппаратных средств для согласованных нагрузок или хранения до размера путей данных между ядрами, памятью и шинами ввода-вывода, таких как PCIe. то есть между различными уровнями кеша и между кешами отдельных ядер. Контроллеры памяти являются частью центрального процессора в современных разработках, поэтому даже доступ к памяти устройства PCIe должен проходить через системный агент процессора. (Это даже позволяет eDRAM L4 Skylake (недоступно ни в одном настольном процессоре:() работает как кэш на стороне памяти (в отличие от Broadwell, который использовал его как кэш-память для L3 IIRC), находясь между памятью и всем остальным в системе, так он может даже кешировать DMA).
Это означает, что аппаратное обеспечение ЦП может делать все необходимое, чтобы убедиться, что хранилище или загрузка атомарны по отношению ко всему остальному в системе, которое может это наблюдать. Это, вероятно, не так много, во всяком случае. Память DDR использует достаточно широкую шину данных, так что 64-битное хранилище действительно электрически переходит по шине памяти на DRAM все в одном и том же цикле. (забавный факт, но не важно. Протокол последовательной шины, такой как PCIe, не помешает ему стать атомарным, пока одно сообщение достаточно велико. А поскольку контроллер памяти - это единственное, что может напрямую взаимодействовать с DRAM, не имеет значения, что он делает внутри, просто размер передачи между ним и остальной частью процессора). Но в любом случае, это "бесплатная" часть: временная блокировка других запросов не требуется для атомарной передачи атома.
x86 гарантирует, что согласованные нагрузки и хранилища до 64 бит являются атомарными, но не более широкими доступами. Реализации с низким энергопотреблением могут разбивать векторные нагрузки / хранилища на 64-битные порции, как это делал P6 с PIII до Pentium M.
Атомные операции происходят в кеше
Помните, что атомное означает, что все наблюдатели видят, что это произошло или не произошло, а не частично произошло. Нет требования, чтобы он сразу достигал основной памяти (или вообще, если перезаписывается в ближайшее время). Для атомарного изменения или чтения кэша L1 достаточно, чтобы любое другое ядро или доступ DMA увидели, что выровненное хранилище или загрузка произошли как одна атомарная операция. Хорошо, если эта модификация произойдет спустя много времени после выполнения хранилища (например, отложено из-за неправильного выполнения до тех пор, пока хранилище не выйдет).
Современные процессоры, такие как Core2 с 128-битными трактами, обычно имеют атомные SSE 128b загрузки / хранения, выходящие за рамки того, что гарантирует ISA x86. Но обратите внимание на интересное исключение для мульти-сокетов Opteron, вероятно, из-за гипертранспорта. Это доказательство того, что атомарно модифицированного кэша L1 недостаточно для обеспечения атомарности хранилищ, более широких, чем самый узкий путь данных (который в данном случае не является путем между кэшем L1 и исполнительными блоками).
Выравнивание важно: загрузка или хранение, которые пересекают границу строки кэша, должны выполняться в два отдельных доступа. Это делает его неатомным.
x86 гарантирует, что кэшированные обращения до 8 байтов являются атомарными, если они не пересекают границу 8B на AMD/Intel. (Или для Intel только на P6 и позже, не пересекайте границу строки кэша). Это означает, что целые строки кэша (64B на современных процессорах) атомарно передаются по Intel, хотя это шире, чем пути данных (32B между L2 и L3 на Haswell/Skylake). Эта атомарность не является полностью "бесплатной" в аппаратном обеспечении и, возможно, требует некоторой дополнительной логики, чтобы предотвратить чтение нагрузки строкой кэша, которая передается только частично. Несмотря на то, что передача строк кэша происходит только после того, как старая версия была признана недействительной, ядро не должно читать из старой копии, пока происходит передача. AMD может разорвать на практике меньшие границы, возможно, из-за использования другого расширения MESI, которое может передавать грязные данные между кешами.
Для более широких операндов, таких как атомарная запись новых данных в несколько элементов структуры, вам нужно защитить их блокировкой, к которой относятся все обращения к ней. (Вы можете использовать x86 lock cmpxchg16b
с циклом повторения, чтобы сделать атомарный 16b магазина. Обратите внимание, что нет способа эмулировать его без мьютекса.)
Атомное чтение-изменение-запись - вот где все сложнее
связанный: мой ответ на Может ли num++ быть атомарным для 'int num'? подробнее об этом.
Каждое ядро имеет собственный кэш L1, который согласован со всеми другими ядрами (используя протокол MOESI). Строки кэша передаются между уровнями кэша и основной памяти кусками, размер которых варьируется от 64 до 256 бит. (эти передачи могут на самом деле быть атомарными на гранулярности всей строки кэша?)
Чтобы сделать атомарный RMW, ядро может поддерживать строку кэша L1 в состоянии Modified без принятия каких-либо внешних изменений в поврежденной строке кэша между загрузкой и хранилищем, а остальная система будет видеть операцию как атомарную. (И, таким образом, он является атомарным, потому что обычные правила выполнения вне очереди требуют, чтобы локальный поток воспринимал свой собственный код как выполняющийся в программном порядке.)
Это можно сделать, не обрабатывая никаких сообщений о когерентности кэша, пока атомный RMW находится в полете (или какой-то более сложной версии, которая допускает больший параллелизм для других операций).
Unaligned lock
Проблемы с редактированием: нам нужны другие ядра, чтобы изменения в двух строках кэша происходили как одна атомарная операция. Это может потребовать фактического сохранения в DRAM и взятия блокировки шины. (Руководство по оптимизации AMD говорит, что это то, что происходит на их процессорах, когда кеш-блокировки недостаточно).
Сигнал LOCK# (вывод пакета / сокета процессора) использовался на старых чипах (для LOCK
с префиксом атомарных операций), теперь есть блокировка кеша. И для более сложных атомных операций, таких как .exchange
или же .fetch_add
вы будете работать с LOCK
префикс или какой-то другой вид атомарной инструкции (cmpxchg/8/16?).
То же руководство, часть Руководства по системному программированию:
В процессорах семейства Pentium 4, Intel Xeon и P6 операция блокировки выполняется с помощью блокировки кеша или шины. Если доступ к памяти кэшируется и затрагивает только одну строку кэша, вызывается блокировка кэша, а системная шина и фактическое расположение памяти в системной памяти не блокируются во время операции.
Вы можете проверить документы и книги Пола МакКенни: * Упорядочение памяти в современных микропроцессорах, 2007 * Барьеры памяти: аппаратное представление для хакеров программного обеспечения, 2010 * perfbook, " Трудно ли выполнять параллельное программирование, и если да, что вы можете сделать с этим Это?
И * Intel 64 Архитектура Память Заказ Памяти, 2007.
Для x86/x86_64 необходим барьер памяти, чтобы предотвратить изменение порядка загрузки. Из первой статьи:
x86 (..AMD64 совместим с x86..) Поскольку процессоры x86 обеспечивают "упорядочение процессов", так что все процессоры согласовывают порядок записи данного процессора в память,
smp_wmb()
примитив не предназначен для процессора [7]. Тем не менее, директива компилятора необходима для предотвращения выполнения компилятором оптимизаций, которые привели бы к переупорядочению черезsmp_wmb()
примитивный.С другой стороны, процессоры x86 традиционно не дают никаких гарантий упорядочения для нагрузок, поэтому
smp_mb()
а такжеsmp_rmb()
примитивы расширяются доlock;addl
, Эта атомарная инструкция действует как барьер для грузов и хранилищ.
Что читает барьер памяти (из второй статьи):
Эффект этого состоит в том, что порядок барьера чтения памяти загружается только на процессор, который его выполняет, так что все нагрузки, предшествующие барьеру чтения памяти, будут завершены до любой нагрузки, следующей за барьером чтения памяти.
Например, из "Белой книги по заказу памяти для архитектуры Intel 64"
Упорядочение памяти в Intel 64 гарантирует, что для каждой из следующих инструкций доступа к памяти выполняемая операция памяти будет выполняться как единая память, независимо от типа памяти: ... Инструкции, которые читают или записывают двойное слово (4 байта), адрес которого выровнен по границе 4 байта.
Упорядочение памяти Intel 64 подчиняется следующим принципам: 1. Нагрузки не переупорядочиваются с другими нагрузками. ... 5. В многопроцессорной системе упорядочение памяти подчиняется причинности (упорядочение памяти учитывает транзитивную видимость). ... Intel 64 упорядочение памяти гарантирует, что нагрузки отображаются в программном порядке
Кроме того, определение mfence
: http://www.felixcloutier.com/x86/MFENCE.html
Выполняет сериализацию для всех команд загрузки из памяти и хранения в память, которые были выполнены до инструкции MFENCE. Эта операция сериализации гарантирует, что каждая инструкция загрузки и сохранения, которая предшествует инструкции MFENCE в программном порядке, становится видимой глобально перед любой инструкцией загрузки или сохранения, которая следует за инструкцией MFENCE.