Атомность на x86

8.1.2 Блокировка шины

Процессоры Intel 64 и IA-32 обеспечивают сигнал LOCK #, который утверждается автоматически во время определенных операций с критической памятью, чтобы заблокировать системной шины или эквивалентной ссылки. Хотя этот выходной сигнал утверждается, запросы от других процессоров или агентов шины для управления шиной блокируются. Программное обеспечение может указывать другие случаи, когда LOCK семантика должна сопровождаться добавлением префикса LOCK к инструкция.

Он исходит из руководства Intel, том 3

Похоже, что атомные операции в памяти будут выполняться непосредственно в памяти (ОЗУ). Я смущен, потому что я вижу "ничего особенного" при анализе сборки. В принципе, выход сборки, сгенерированный для std::atomic<int> X; X.load(), ставит только "лишнее" mfence. Но он отвечает за правильное упорядочение памяти, а не за атомарность. Если я правильно понимаю, X.store(2) просто mov [somewhere], $2. И это все. Кажется, что он не "пропускает" кеш. Я знаю, что перемещение (например, ints) в память является атомарным. Однако я смущен.


Итак, я представил свои сомнения, но главный вопрос:

Как ЦПУ реализует атомарные операции внутри?

Ответ 1

Похоже, что атомные операции в памяти будут выполняться непосредственно в памяти (ОЗУ).

Нет, до тех пор, пока все возможные наблюдатели в системе воспринимают операцию как атомарную, операция может включать только кеш. Удовлетворение этого требования намного сложнее для операций атомарного чтения-модификации-записи (например, lock add [mem], eax, особенно с несогласованным адресом), то есть когда ЦП может утверждать сигнал LOCK #. Вы все еще не увидите больше, чем в asm: аппаратное обеспечение реализует требуемую ISA семантику для инструкций lock ed.

Хотя я сомневаюсь, что на современных процессорах, где контроллер памяти встроен в CPU, есть физический внешний вывод 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. Раньше (non 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's, используя xchg, чтобы сделать хранилище и барьер памяти с той же инструкцией. (На некоторых процессорах, особенно AMD, инструкция lock ed в качестве барьера может быть дешевле, чем MFENCE, потому что IIRC AMD документирует дополнительную семантику сериализации-конвейера (для выполнения команд, а не только для упорядочения памяти) для MFENCE).


Этот вопрос выглядит как часть 2 вашей предыдущей модели памяти в С++: последовательная согласованность и атомарность, где вы спросили:

Как ЦПУ реализует атомарные операции внутри?

Как вы отметили в вопросе, атомарность не связана с упорядочением по отношению к любым другим операциям. (т.е. memory_order_relaxed). Это просто означает, что операция выполняется как одна неделимая операция, отсюда имя, а не как несколько частей, которые могут произойти частично до и частично после чего-то иначе.

Вы получаете атомарность "бесплатно" без дополнительного оборудования для согласованных нагрузок или сохраняете до размера путей передачи данных между ядрами, памятью и шинами ввода-вывода, такими как PCIe., т.е. между различные уровни кеша и между кэшами отдельных ядер. Контроллеры памяти являются частью процессора в современных конструкциях, поэтому даже устройство PCIe, использующее память, должно пройти через системный агент ЦП. (Это даже позволяет Skylake eDRAM L4 (недоступно ни в одном настольном процессоре:() работает как кеш памяти (в отличие от Broadwell, который использовал его в качестве кэша-жертвы для L3 IIRC), сидящего между памятью и всем остальным в системе, поэтому он может даже кэшировать DMA).

Skylake system agent diagram, from IDF via ARStechnica

Это означает, что аппаратное обеспечение ЦП может делать все, что необходимо для обеспечения того, чтобы хранилище или загрузка были атомарными по отношению к чему-либо еще в системе, которые могут его наблюдать. Наверное, это не так много. DDR-память использует достаточно широкую шину данных, которая в 64-битном выровненном хранилище действительно электрически переводит шину памяти в DRAM все в одном цикле. (забавный факт, но неважный). Протокол последовательной шины, такой как PCIe, не помешает ему быть атомарным, если одно сообщение достаточно велико. И поскольку контроллер памяти - это единственное, что можно напрямую связать с DRAM, неважно, что он делает внутренне, просто размер передачи между ним и остальной частью ЦП). Но в любом случае это "бесплатная" часть: для сохранения атомного переноса атома не требуется временная блокировка других запросов.

x86 гарантирует, что согласованные нагрузки и хранилища до 64 бит являются атомарными, но не более широкий доступ. Маломощные реализации могут разбить векторные нагрузки/хранилища на 64-битные куски, такие как P6, с PIII до Pentium M.


Атомные операторы выполняются в кеше

Помните, что атомный просто означает, что все наблюдатели считают, что это произошло или не произошло, никогда не произошло частично. Там нет требования, чтобы он фактически дошел до основной памяти сразу (или вообще, если перезаписано в ближайшее время). Атомно модифицировать или читать кеш L1 достаточно, чтобы гарантировать, что любой другой ядро ​​или доступ к DMA будет видеть выравниваемое хранилище или загрузка, выполняются как одна атомная операция. Хорошо, если эта модификация происходит задолго после того, как хранилище выполняется ( например, задерживается исполнением вне порядка до закрытия магазина).

Современные процессоры, такие как Core2 со 128-битными путями повсюду, как правило, имеют атомные SSE 128b нагрузки/хранилища, выходящие за пределы того, что гарантирует x86 ISA. Но обратите внимание на интересное исключение на многопроцессорном Opteron, вероятно, из-за гипертранспорта. Это доказательство того, что атомарно модифицирующий кеш L1 недостаточно, чтобы обеспечить атомарность для магазинов, более широких, чем самые узкие путь данных (в этом случае это не путь между кешем L1 и исполнительными блоками).

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

x86 гарантирует, что кешированные обращения до 8 байтов являются атомарными, если они не пересекают границу 8B на AMD/Intel. (Или для Intel только на P6 и более поздних версиях, не пересекайте границу линии кэша). Это означает, что целые строки кэша (64B на современных процессорах) передаются атомарно на Intel, хотя это шире, чем пути данных (32B между L2 и L3 на Haswell/Skylake). Эта атомарность не является полностью "свободной" в аппаратном обеспечении и, возможно, требует некоторой дополнительной логики, чтобы предотвратить загрузку от чтения строки кэша, которая только частично передана. Хотя передача в кеш-строку происходит только после того, как старая версия была признана недействительной, так что ядро ​​не должно читать из старой копии, пока происходит передача. AMD может повредить на практике меньшие границы, возможно, из-за использования другого расширения для MESI, которое может передавать грязные данные между кешами.

Для более широких операндов, таких как атомарная запись новых данных в несколько записей структуры, вам необходимо защитить ее с помощью блокировки, к которой все обращаются к ней. (Вы можете использовать x86 lock cmpxchg16b с циклом повтора для создания атомного 16b-хранилища. Обратите внимание, что нет способа эмулировать его без мьютекса.)


Atomic read-modify-write - это то, где становится сложнее

related: мой ответ на Может ли num ++ быть атомарным для 'int num'? подробнее об этом.

Каждое ядро ​​имеет частный кеш L1, который является когерентным со всеми другими ядрами (с использованием протокола MOESI). Кэш-линии передаются между уровнями кеша и основной память в кусках размером от 64 бит до 256 бит. (эти передачи могут фактически быть атомарными по всей детализации в кеш-строке?)

Чтобы сделать атомный RMW, ядро ​​может поддерживать линию кэша L1 в измененном состоянии без принятия каких-либо внешних изменений в затронутой строке кэша между загрузкой и хранилищем, остальная часть системы увидит, что операция является атомной. (И, следовательно, он является атомарным, поскольку обычные правила выполнения вне порядка требуют, чтобы локальный поток видел, что его собственный код работает в порядке выполнения программы.)

Он может сделать это, не обрабатывая сообщения о кеш-когерентности, когда атомный RMW находится в полете (или более сложная версия, которая позволяет больше parallelism для других операционных систем).

Unaligned lock ed ops - проблема: нам нужны другие ядра, чтобы увидеть, что изменения в двух строках кэша происходят как одна атомная операция. Это может потребовать фактического хранения DRAM и блокировки шины. (Руководство по оптимизации AMD говорит, что это то, что происходит на их процессорах, когда недостаточно блокировки кеша.)

Ответ 2

Сигнал LOCK # (вывод процессора/сокета процессора) использовался на старых чипах (для LOCK префиксов атомных операций), теперь существует блокировка кеша. А для более сложных атомных операций, таких как .exchange или .fetch_add, вы будете работать с LOCK prefix или какой-либо другой вид атомной инструкции (CMPXCHG/8/16?).

То же руководство, часть руководства по системному программированию:

В процессорах семейства Pentium 4, Intel Xeon и P6 операция блокировки выполняется либо с блокировкой кеша, либо с блокировкой шины. Если доступ к памяти кэшируется и затрагивает только одну строку кэша, вызывается блокировка кеша, а системная шина и фактическое расположение памяти в системной памяти не блокируются во время операции.

Вы можете проверить документы и книги у Пола Э. МакКенни: * Заказ памяти в современных микропроцессорах, 2007 * Барьеры памяти: аппаратное обеспечение для программных хакеров, 2010 * perfbook, Является ли параллельное программирование жестким, а если так, то Можете ли вы сделать об этом? "

А * Техническая документация по хранению памяти Intel 64 Architecture, 2007 г.

Для защиты x86/x86_64 требуется защита памяти, чтобы предотвратить переупорядочение нагрузки. Из первой статьи:

x86 (..AMD64 совместим с x86..) Поскольку процессоры x86 обеспечивают "упорядочение процессов", так что все процессоры согласуются с порядком записи данных в память CPU, smp_wmb() primitive - это не-op для CPU [7]. Однако для предотвращения компилятора от оптимизации требуется преобразование, которое приведет к переупорядочению по примитиву smp_wmb().

С другой стороны, процессоры x86 традиционно не давали никаких заказов на загрузку, поэтому примитивы smp_mb() и smp_rmb() расширяются до lock;addl. Эта атомная инструкция действует как барьер как для нагрузок, так и для хранилищ.

Что читает барьер памяти (со второй бумаги):

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

Например, из "Белой книги по заказу архитектуры Intel 64 Architecture"

Сохранение памяти Intel 64 гарантирует, что для каждой из следующих инструкций по доступу к памяти операция составной памяти, как представляется, выполняется как один доступ к памяти независимо от типа памяти:... Инструкции, которые читают или записывают двойное слово (4 байта) адрес которого выровнен по границе 4 байта.

Порядок хранения памяти Intel 64 подчиняется следующим принципам: 1. Нагрузки не переупорядочиваются с другими нагрузками.... 5. В многопроцессорной системе упорядочение памяти подчиняется причинности (порядок запоминания учитывает транзитивную видимость).... Сохранение памяти Intel 64 гарантирует, что нагрузки видны в программном порядке

Кроме того, определение mfence: http://www.felixcloutier.com/x86/MFENCE.html

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