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

c++ multithreading x86 atomic memory-barriers

5446 просмотра

2 ответа

1712 Репутация автора

8.1.2 Блокировка автобуса

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

Он взят из Руководства Intel, Том 3

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


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

Как процессор выполняет атомарные операции внутри?

Автор: Gilgamesz Источник Размещён: 18.07.2016 11:02

Ответы (2)


1 плюс

60745 Репутация автора

Сигнал 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.

Автор: osgx Размещён: 19.07.2016 04:46

26 плюса

164176 Репутация автора

Решение

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

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

Хотя я сомневаюсь, что на современных процессорах есть физический внешний контакт 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).


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

Как процессор выполняет атомарные операции внутри?

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

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

Диаграмма агента системы Skylake, от IDF через ARStechnica

Это означает, что аппаратное обеспечение ЦП может делать все необходимое, чтобы убедиться, что хранилище или загрузка атомарны по отношению ко всему остальному в системе, которое может это наблюдать. Это, вероятно, не так много, во всяком случае. Память 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 находится в полете (или какой-то более сложной версии, которая допускает больший параллелизм для других операций).

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

Автор: Peter Cordes Размещён: 19.07.2016 05:53
Вопросы из категории :
32x32