Сравнить и поменять местами C ++ 0x

Из предложения C ++ 0x об атомарных типах и операциях C ++:

29.1 Порядок и последовательность [atomics.order]

Добавьте новый подпункт со следующими абзацами.

Перечисление memory_order определяет подробный регулярный (неатомарный) порядок синхронизации памяти, как определено в [новый раздел, добавленный N2334 или его принятым преемником], и может обеспечивать порядок операций. Его перечисленные значения и их значения следующие.

  • memory_order_relaxed

Операция не упорядочивает память.

  • memory_order_release

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

  • memory_order_acquire

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

  • memory_order_acq_rel

Операция имеет семантику как получения, так и освобождения.

  • memory_order_seq_cst

Операция имеет семантику получения и освобождения и, кроме того, имеет последовательно согласованный порядок операций.

Ниже в предложении:

bool A::compare_swap( C& expected, C desired,
        memory_order success, memory_order failure ) volatile

где можно указать порядок памяти для CAS.


Я понимаю, что «memory_order_acq_rel» обязательно синхронизирует только те ячейки памяти, которые необходимы для операции, в то время как другие ячейки памяти могут оставаться несинхронизированными (это не будет вести себя как ограждение памяти).

Теперь у меня вопрос: если я выберу «memory_order_acq_rel» и применяю compare_swap к целым типам, например, целым числам, как это обычно переводится в машинный код на современных потребительских процессорах, таких как многоядерный Intel i7? А как насчет других часто используемых архитектур (x64, SPARC, ppc, arm)?

В частности (при условии конкретного компилятора, скажем, gcc):

  1. Как сравнить и поменять местами целочисленное местоположение с помощью вышеуказанной операции?
  2. Какую последовательность инструкций будет производить такой код?
  3. Работает ли на i7 без блокировки?
  4. Будет ли такая операция запускать протокол полной согласованности кеша, синхронизируя кеши разных ядер процессора, как если бы это была ограда памяти на i7? Или он просто синхронизирует участки памяти, необходимые для этой операции?
  5. В связи с предыдущим вопросом - есть ли какое-либо преимущество в производительности при использовании семантики acq_rel на i7? А как насчет других архитектур?

Спасибо за ответы на все вопросы.


person axel22    schedule 18.11.2010    source источник
comment
Из предложения C ++ 0x по атомарным типам и операциям C ++: Приведенный вами текст является действительно очень плохим объяснением.   -  person curiousguy    schedule 09.12.2019


Ответы (2)


Ответ здесь нетривиальный. То, что именно происходит и что имеется в виду, зависит от многих вещей. Для базового понимания когерентности / памяти кеша, возможно, могут быть полезны мои недавние записи в блоге:

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

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 вместо locked инструкций, < 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
comment
Я также должен добавить, что явная инструкция по ограждению обычно не используется. Семантики блокировки, неявной блокировки некоторых функций и гарантий упорядочивания обычно достаточно. - person edA-qa mort-ora-y; 18.11.2010
comment
Спасибо за подробный ответ. 1) Моя самая большая проблема с блокировками - это вытеснение потоков. Поскольку lock в lock cmpxchg на самом деле не блокировка, а аннотация семантики., lock cmpxchg выполняется сразу. 2) Следующее, что меня беспокоит, это то, что lock cmpxchg фактически сбрасывает буфер в память - судя по тому, что вы сказали и написали в блоге, этого не происходит на новых процессорах. 3) Кроме того, мне кажется, что меньше разногласий, когда 2 ядра выполняют атомарные операции в 2 отдельных удаленных местах памяти, поскольку нет перезагрузки. Это верно? Хорошие сообщения в блоге, кстати. - person axel22; 18.11.2010
comment
Вы писали: Если два ядра имеют одно и то же место в памяти, кэшированное в строке кэша, одно будет помечено как грязное, а другое будет перезагружено по мере необходимости. И аналогичная вещь в вашем блоге. С другой стороны, в этом вопросе stackoverflow.com/questions/4213639/ пользователь утверждает: Но, если A выполняет обычную запись 0 по адресу 0x100, тогда B записывает 1 в 0x100, а затем оба C&S по адресу 0x200 - после этого они оба увидят одно и то же значение в 0x200, но A может все еще думать, что 0x100 содержит 0. Разве эти два утверждения не противоречат друг другу? - person axel22; 18.11.2010
comment
Последний комментарий, если, конечно, предположить, что, говоря о перезагрузке строки кэша, вы имели в виду, что это происходит с обычными загрузками и сохранениями, а не с теми, которые помечены как атомарные. - person axel22; 18.11.2010
comment
Большинство основных операций не сбрасываются в память. Если вы явно не укажете процессору сделать это, он, как правило, не будет сбрасываться в память, пока не наступит подходящее время для этого - и это, скорее всего, не будет мешать вашей программе. - person edA-qa mort-ora-y; 18.11.2010
comment
Спасибо за эти разъяснения! - person axel22; 18.11.2010
comment
Flush предлагает очистить кеш и записать значения в RAM. Практически никакой программе это делать не нужно. Кроме кода Spectre! - person curiousguy; 09.12.2019
comment
Я внес некоторые правки, чтобы прояснить некоторые неверные утверждения и связать некоторые статьи Джеффа Прешинга о барьерах памяти. Самое главное, что mo_relaxed по-прежнему нужен префикс lock; это не только для упорядочивания, это также необходимо для атомарности всего чтения-изменения-записи! Но также то, что это кажется очень ориентированным на x86 и не дает хорошего объяснения барьеров, кроме полного барьера. (Хотя на самом деле я этого не исправлял.) Но да, кэш согласован; люди часто объясняют упорядочение памяти ерундой, например, кешем, содержащим конфликтующие значения, а не буфером хранилища, задерживающим фиксацию до L1d. - person Peter Cordes; 10.12.2019

Все ваше мировоззрение кажется необоснованным: ваш вопрос намекает на то, что согласованность кеша контролируется порядками памяти на уровне C ++ и ограждениями или атомарными операциями на уровне ЦП.

Но согласованность кеша - один из наиболее важных инвариантов физической архитектуры, и он всегда обеспечивается системой памяти, которая состоит из взаимосвязи всех процессоров и оперативной памяти. Вы никогда не сможете превзойти его с помощью кода, запущенного на процессоре, или даже увидеть детали его работы. Конечно, наблюдая непосредственно за ОЗУ и выполняя код в другом месте, вы можете увидеть устаревшие данные на каком-то уровне памяти: по определению ОЗУ не имеет самого нового значения из всех ячеек памяти.

Но код, работающий на ЦП, не может получить доступ к DRAM напрямую, только через иерархию памяти, которая включает кеши, которые взаимодействуют друг с другом для поддержания согласованности этого общего представления памяти. (Обычно с MESI). Даже на одном ядре кэш с обратной записью позволяет значениям DRAM быть устаревшими, что может быть проблемой для некогерентного DMA, но не для чтения / записи памяти из ЦП.

Таким образом, проблема существует только для внешних устройств, и только для тех, которые выполняют некогерентный DMA. (DMA согласован с кешем на современных процессорах x86; контроллер памяти, встроенный в процессор, делает это возможным).

Будет ли такая операция запускать протокол полной согласованности кешей, синхронизируя кеши разных ядер процессора, как если бы это была ограда памяти на i7?

Они уже синхронизированы. См. Обеспечивает ли барьер памяти, что согласованность кеша завершена? - барьеры памяти выполняют только локальные действия внутри ядра, выполняющего барьер, например, очищают буфер хранилища.

Или он просто синхронизирует участки памяти, необходимые для этой операции?

Атомарная операция применяется ровно к одной ячейке памяти. Какие еще места вы имеете в виду?

На слабо упорядоченном процессоре memory_order_relaxed атомарное приращение могло бы избежать того, чтобы предыдущие загрузки / сохранения были видимыми до этого приращения. Но строго упорядоченная модель памяти x86 этого не позволяет.

person curiousguy    schedule 08.12.2019