Сравните и поменяйте местами C ++ 0x - PullRequest
16 голосов
/ 18 ноября 2010

Из предложения 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? А как насчет других архитектур?

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

1 Ответ

7 голосов
/ 18 ноября 2010

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

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

compare_swap( C& expected, C desired,
        memory_order success, memory_order failure )

Архитектура не сможет реализовать это в точности так, как вы просили. Когда вы указываете memory_order, вы указываете, как может работать переупорядочение. Чтобы использовать термины Intel, вы будете указывать, какой тип забора вы хотите, есть три из них, полный забор, грузовой забор, и забор магазина. Если вы хотите, чтобы для этой операции была определенная граница, это не означает, что она поддерживается, и я надеюсь, что она всегда будет полностью ограждена.

Компилятор, вероятно, будет использовать инструкцию CMPXCHG для реализации вызова. Если вы указали что-то отличное от relaxed, оно пометит это как lock, чтобы указать, что функция должна быть атомарной. Является ли это «свободным от блокировки», очень сильно зависит от того, о чем вы думаете в терминах «блокировки».

С точки зрения синхронизации памяти вы должны понимать, как работает когерентность кэша (мой блог может немного помочь). Новые процессоры используют архитектуру ccNUMA (ранее SMP). По сути, «представление» в памяти никогда не нарушается. Ограждения, используемые в коде, на самом деле не приводят к самопроизвольному сбросу. Если два ядра имеют одно и то же место памяти, кэшированное в строке кэша, одно будет помечено как грязное, а другое перезагрузится при необходимости. Очень простое объяснение очень сложного процесса

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

- Ответы на некоторые комментарии:

Вы должны различать, что означает выполнение инструкции записи и запись в ячейку памяти. Это то, что я пытаюсь объяснить в своем блоге. К тому времени, когда «0» фиксируется в 0x100, все ядра видят этот ноль. Запись целых чисел также атомарна, то есть даже без блокировки, когда вы пишете в местоположение, все ядра сразу же получат это значение, если захотят его использовать.

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

Что касается противоречивых утверждений, в общем случае вы увидите все виды утверждений. Будь они противоречивыми, все сводится к тому, что именно означает «увидеть», «загрузить», «выполнить» в контексте. Если вы записываете «1» в 0x100, означает ли это, что вы выполнили инструкцию записи или ЦП фактически зафиксировал это значение. Разница заключается в переупорядочении. Процессор может задержать запись «1», но вы можете быть уверены, что в тот момент, когда он, наконец, передаст «1», все ядра увидят его. Заборы контролируют этот порядок.

...