Подтвердить что ты не робот

Сравнение и обмен С++ 0x

Из Предложение С++ 0x в С++ Atomic Types и Operations:

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):

  • Как сравнить и поменять целое местоположение с указанной выше операцией?
  • Какая последовательность команд будет вызывать такой код?
  • Является ли операция блокировкой на i7?
  • Будет ли такая операция работать с протоколом когерентности полного кеша, синхронизируя кеши разных процессорных ядер, как если бы это был забор памяти на i7? Или он просто синхронизирует ячейки памяти, необходимые для этой операции?
  • В связи с предыдущим вопросом - есть ли преимущество в производительности для использования семантики acq_rel на i7? Как насчет других архитектур?

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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