Почему хранилище std::atomic с последовательной последовательностью использует XCHG?
Почему std::atomic
"s store
:
std::atomic<int> my_atomic;
my_atomic.store(1, std::memory_order_seq_cst);
делать xchg
когда запрашивается магазин с последовательной последовательностью?
Технически, разве обычного хранилища с барьером для чтения / записи может быть недостаточно? Эквивалентно:
_ReadWriteBarrier(); // Or `asm volatile("" ::: "memory");` for gcc/clang
my_atomic.store(1, std::memory_order_acquire);
Я явно говорю о x86 и x86_64. Там, где у магазина есть скрытый забор.
1 ответ
mov
-магазин + mfence
а также xchg
оба являются допустимыми способами реализации хранилища последовательной согласованности на x86. Неявный lock
префикс на xchg
с памятью делает его полным барьером памяти, как и все атомарные операции RMW на x86. (К сожалению, для других случаев использования, x86 не предоставляет способ сделать ослабленный или acq_rel атомарный инкремент, только seq_cst.)
гладкий mov
недостаточно; у него есть только семантика выпуска, а не последовательный выпуск. (В отличие от AArch64's stlr
инструкция, которая делает магазин последовательного выпуска. Этот выбор, очевидно, мотивирован тем, что C++11 имеет seq_cst в качестве порядка памяти по умолчанию. Но обычный магазин AArch64 намного слабее; расслаблен, не выпущен.) См . статью Джеффа Прешинга о семантике получения / выпуска и обратите внимание, что регулярный выпуск позволяет переупорядочивать с последующими операциями. (Если хранилище релизов снимает блокировку, нормально, чтобы более поздние вещи появлялись внутри критической секции.)
Есть различия в производительности между mfence
а также xchg
на разных процессорах, и, возможно, в горячей и холодной кэш-памяти и в спорных и неконтролируемых случаях. И / или для обеспечения пропускной способности многих операций в одном потоке подряд, а также для одной и для того, чтобы позволить окружающему коду перекрывать выполнение с атомарной операцией.
На оборудовании Intel Skylake, mfence
блокирует неупорядоченное выполнение независимых инструкций ALU, но xchg
не ( Смотрите мой тест asm + результаты внизу этого SO ответа). Руководства Intel не требуют, чтобы он был таким сильным; только lfence
задокументировано, чтобы сделать это. Но в качестве детали реализации, это очень дорого для неупорядоченного исполнения окружающего кода на Skylake.
Я не тестировал другие процессоры, и это может быть результатом исправления микрокода для ошибки SKL079, SKL079 MOVNTDQA из памяти WC может пройти ранее инструкции MFENCE. Наличие ошибки в основном доказывает, что SKL раньше мог выполнять инструкции после MFENCE. Я не удивлюсь, если они исправят это, сделав MFENCE сильнее в микрокоде, своего рода тупой инструментальный подход, который значительно увеличивает влияние на окружающий код.
Я только что протестировал однопоточный случай, когда строка кеша горячая в кеше L1d. (Не тогда, когда в памяти холодно или когда оно находится в состоянии Modified на другом ядре.) xchg
должен загрузить предыдущее значение, создавая "ложную" зависимость от старого значения, которое было в памяти. Но mfence
заставляет ЦП ждать, пока предыдущие хранилища не передадут L1d, что также требует, чтобы строка кэша прибыла (и была в состоянии М). Так что они, вероятно, примерно равны в этом отношении, но Intel mfence
заставляет все ждать, а не только загружает.
Руководство по оптимизации AMD рекомендует xchg
для атомных магазинов. Я думал, что Intel рекомендовал mov
+ mfence
, который использует gcc, но компилятор Intel также использует xchg
Вот.
Когда я проверил, я получил лучшую пропускную способность на Skylake для xchg
чем для mov
+ mfence
в однопоточном цикле в том же месте несколько раз. См . Руководство по микроарху Agner Fog и таблицы инструкций для некоторых деталей, но он не тратит много времени на заблокированные операции.
Смотрите вывод gcc/clang/ICC/MSVC в проводнике компилятора Godbolt для C++11 seq-cst my_atomic = 4;
GCC использует mov
+ mfence
когда SSE2 доступен. (использование -m32 -mno-sse2
получить GCC для использования xchg
тоже). Остальные 3 компилятора предпочитают xchg
с настройкой по умолчанию, или для znver1
(Ryzen) или skylake
,
Ядро Linux использует xchg
за __smp_store_mb()
,
Таким образом, похоже, что GCC должен использовать xchg
Если только у них нет результатов теста, о которых никто не знает.
Еще один интересный вопрос, как скомпилировать atomic_thread_fence(mo_seq_cst);
, Очевидный вариант mfence
, но lock or dword [rsp], 0
является еще одним допустимым вариантом (и используется gcc -m32
когда MFENCE недоступен). Дно стека обычно уже горячо в кеше в состоянии М. Недостатком является введение задержки, если локальный хранился там. (Если это просто обратный адрес, прогнозирование обратного адреса, как правило, очень хорошее, поэтому задержка ret
способность читать это не большая проблема.) Так lock or dword [rsp-4], 0
может стоить рассмотреть в некоторых случаях. ( gcc действительно рассмотрел это, но отменил это, потому что это делает valgrind несчастным. Это было до того, как стало известно, что это может быть лучше, чем mfence
даже когда mfence
был доступен.)
Все компиляторы в настоящее время используют mfence
для отдельного барьера, когда он доступен. Это редко встречается в коде C++11, но необходимы дополнительные исследования того, что на самом деле наиболее эффективно для реального многопоточного кода, который действительно работает внутри потоков, которые взаимодействуют без блокировки.
Но несколько источников рекомендуют использовать lock add
в стек как барьер вместо mfence
поэтому ядро Linux недавно переключилось на использование его для smp_mb()
реализация на x86, даже когда SSE2 доступен.
См. https://groups.google.com/d/msg/fa.linux.kernel/hNOoIZc6I9E/pVO3hB5ABAAJ для некоторых обсуждений, включая упоминание некоторых ошибок для HSW/BDW о movntdqa
загружается из памяти WC, проходящей ранее lock
Инструкции ред. (Напротив Скайлэйк, где это было mfence
вместо lock
инструкции, которые были проблемой. Но в отличие от SKL, микрокодов не исправить. Это может быть, почему Linux все еще использует mfence
для его mb()
для драйверов, в случае, если что-либо когда-либо использует загрузку NT для копирования обратно из видеопамяти или что-то в этом роде, но не может позволить выполнить чтение до тех пор, пока не появится более раннее хранилище.)
В Linux 4.14
smp_mb()
использованияmb()
, Использует mfence, если доступно, в противном случаеlock addl $0, 0(%esp)
,__smp_store_mb
(магазин + барьер памяти) используетxchg
(и это не изменится в более поздних ядрах).В Linux 4.15
smb_mb()
использованияlock; addl $0,-4(%esp)
или же%rsp
, Вместо того, чтобы использоватьmb()
, (Ядро не использует красную зону даже в 64-битной, поэтому-4
может помочь избежать дополнительной задержки для локальных переменных).mb()
используется драйверами для заказа доступа к регионам MMIO, ноsmp_mb()
превращается в no-op при компиляции для однопроцессорной системы. измененияmb()
рискованнее, потому что его сложнее тестировать (влияет на драйверы), а у процессоров есть ошибки, связанные с блокировкой против mfence. Но в любом случае,mb()
использует mfence, если доступно, иначеlock addl $0, -4(%esp)
, Единственным изменением является-4
,- В Linux 4.16 никаких изменений, кроме удаления
#if defined(CONFIG_X86_PPRO_FENCE)
который определил вещи для более слабо упорядоченной модели памяти, чем модель x86-TSO, которую реализует современное аппаратное обеспечение.
x86 и x86_64. Там, где у магазина есть скрытый забор
Вы имеете в виду выпуск, я надеюсь. my_atomic.store(1, std::memory_order_acquire);
не будет компилироваться, потому что атомарные операции только для записи не могут быть операциями получения. См. Также статью Джеффа Прешинга о семантике получения / выпуска.
Или же
asm volatile("" ::: "memory");
Нет, это только барьер компилятора; он предотвращает все переупорядочения во время компиляции через него, но не препятствует переупорядочению StoreLoad во время выполнения, то есть буферизует хранилище до более позднего периода и не появляется в глобальном порядке до последующей загрузки. (StoreLoad - единственный вид переупорядочения во время выполнения, который позволяет x86.)
В любом случае, еще один способ выразить то, что вы хотите здесь:
my_atomic.store(1, std::memory_order_release); // mov
// with no operations in between, there's nothing for the release-store to be delayed past
std::atomic_thread_fence(std::memory_order_seq_cst); // mfence
Использование ограждения релиза не будет достаточно сильным (оно и хранилище релизов могут быть отложены после более поздней загрузки, что аналогично утверждению, что ограждения релиза не удерживают более поздние загрузки от ранних загрузок). Тем не менее, ограждение с выпуском релиза сделало бы все возможное, чтобы более поздние загрузки не происходили рано, а само по себе не было возможности изменить порядок в хранилище релизов.
Связанный: статья Джеффа Прешинга о заборах, отличающихся от операций выпуска.
Но обратите внимание, что seq-cst является особенным в соответствии с правилами C++11: гарантированно, что только операции seq-cst имеют один глобальный / общий порядок, который все потоки согласны видеть. Таким образом, эмуляция их с помощью более слабых ограждений порядка + в общем случае может быть не совсем эквивалентна на абстрактной машине C++, даже если она на x86. (На x86 все хранилища имеют один общий порядок, с которым согласуются все ядра. См. Также инструкции по загрузке, невидимые глобально: загрузки могут получать свои данные из буфера хранилища, поэтому мы не можем сказать, что существует общий заказ для загрузок + хранилищ..)