Ответ здесь нетривиальный. То, что именно происходит и что имеется в виду, зависит от многих вещей. Для базового понимания когерентности / памяти кеша, возможно, могут быть полезны мои недавние записи в блоге:
Но помимо этого, позвольте мне попытаться ответить на несколько вопросов. Прежде всего, приведенная ниже функция очень надеется на то, что поддерживается: очень детальный контроль над тем, насколько надежна гарантия порядка памяти, которую вы получаете. Это разумно для переупорядочения во время компиляции, но часто не для барьеров во время выполнения.
compare_swap( C& expected, C desired,
memory_order success, memory_order failure )
Не все архитектуры смогут реализовать это в точности так, как вы просили; многим придется усилить его до чего-то достаточно сильного, чтобы они могли реализовать. Когда вы указываете memory_order, вы указываете, как может работать переупорядочение. Используя термины Intel, вы должны указать, какой тип ограждения вам нужен, всего их три: сплошное ограждение, ограждение для груза и ограждение для магазина. (Но на x86 забор загрузки и забор хранилища полезны только со слабо упорядоченными инструкциями, такими как хранилища NT; атомики их не используют. Регулярная загрузка / сохранение дает вам все, за исключением того, что хранилища могут появляться после более поздних загрузок.) Просто потому, что вы хотите конкретный забор на этой операции не будет означать, что он поддерживается, и я надеюсь, что он всегда возвращается к сплошному забору. (См. статью о барьерах памяти)
Компилятор x86 (включая x64), скорее всего, будет использовать инструкцию LOCK CMPXCHG
для реализации CAS, независимо от упорядочивание памяти. Это подразумевает полный барьер; x86 не имеет возможности сделать операцию чтения-изменения-записи атомарной без префикса lock
, что также является полным барьером. Pure-store и pure-load могут быть атомарными «сами по себе», при этом многим ISA требуются барьеры для чего-либо выше mo_relaxed
, но x86 делает acq_rel
" бесплатно "в asm.
Эта инструкция не требует блокировки, хотя все ядра, пытающиеся подключиться к одному и тому же местоположению, будут бороться за доступ к ней, так что вы можете утверждать, что это не совсем без ожидания. (Алгоритмы, которые его используют, могут быть неблокирующими, но сама операция не требует ожидания, см. статью о неблокирующем алгоритме в Википедии). На устройствах, отличных от x86, с LL / SC вместо lock
ed инструкций, < a href = "https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange" rel = "nofollow noreferrer"> C ++ 11 compare_exchange_weak
обычно не требует ожидания, но compare_exchange_strong
требует повторной попытки шлейф в случае ложного отказа.
Теперь, когда C ++ 11 существует уже много лет, вы можете посмотреть вывод asm для различных архитектур в проводнике компилятора Godbolt.
Что касается синхронизации памяти, вам нужно понимать, как работает согласованность кеша (мой блог может немного помочь). Новые процессоры используют архитектуру ccNUMA (ранее SMP). По сути, "взгляд" на память никогда не рассинхронизируется. Ограничения, используемые в коде, на самом деле не вызывают никакого сброса кеша как такового, а только фиксации буфера хранилища в хранилищах полетов в кеш перед последующей загрузкой.
Если два ядра имеют одну и ту же ячейку памяти, кэшированную в строке кэша, хранилище одного ядра получит исключительное владение строкой кеша (аннулируя все другие копии) и пометив свою собственную как грязную. Очень простое объяснение очень сложного процесса
Чтобы ответить на ваш последний вопрос, вы всегда должны использовать семантику памяти, которая, по вашему мнению, должна быть правильной. Большинство архитектур не поддерживают все комбинации, которые вы используете в своей программе. Однако во многих случаях вы получите отличную оптимизацию, особенно в тех случаях, когда запрошенный вами порядок гарантирован без ограждения (что довольно часто).
- Ответы на некоторые комментарии:
Вы должны различать, что означает выполнение инструкции записи и запись в ячейку памяти. Это то, что я пытаюсь объяснить в своем сообщении в блоге. К тому времени, когда "0" зафиксирован на 0x100, все ядра видят этот ноль. Запись целых чисел также является атомарной, то есть даже без блокировки, когда вы записываете в место, все ядра немедленно получат это значение, если они захотят его использовать.
Проблема в том, что для использования значения, которое вы, вероятно, сначала загрузили в регистр, любые изменения местоположения после этого, очевидно, не коснутся регистра. Вот почему нужны мьютексы или atomic<T>
, несмотря на когерентную память кеша: компилятору разрешено хранить простые значения переменных в частных регистрах. (В C ++ 11 это потому, что гонка данных для переменных, отличных от atomic
, является неопределенным поведением.)
Что касается противоречивых утверждений, обычно вы увидите самые разные претензии. Насколько они противоречивы, зависит от того, что именно означает «увидеть», «загрузить», «выполнить» в контексте. Если вы записываете «1» в 0x100, означает ли это, что вы выполнили инструкцию записи или процессор действительно зафиксировал это значение. Разница, создаваемая буфером хранилища, является одной из основных причин переупорядочения (единственное, что позволяет x86). ЦП может отложить запись «1», но вы можете быть уверены, что в тот момент, когда он наконец зафиксирует эту «1», все ядра увидят его. Ограничения управляют этим порядком, заставляя поток ждать, пока хранилище не зафиксируется, прежде чем выполнять последующие операции.
person
edA-qa mort-ora-y
schedule
18.11.2010