В чем разница между атомным и критическим в OpenMP?

В чем разница между атомным и критическим в OpenMP?

я могу сделать это

#pragma omp atomic
g_qCount++;

но разве это не то же самое, что

#pragma omp critical
g_qCount++;

?

9 ответов

Решение

Эффект на g_qCount тот же, но то, что сделано, отличается.

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

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

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

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

Конечно, в любом случае вы несете стоимость сериализации.

В OpenMP все безымянные критические разделы являются взаимоисключающими.

Самое важное различие между критическим и атомарным состоит в том, что атомарный может защитить только одно назначение, и вы можете использовать его с конкретными операторами.

Критический раздел:

  • Обеспечивает сериализацию блоков кода.
  • Может быть расширен для сериализации групп блоков с правильным использованием тега "name".

  • помедленнее!

Атомная операция:

  • Это намного быстрее!

  • Обеспечивает только сериализацию определенной операции.

Самый быстрый способ не является ни критическим, ни атомарным. Примерно, сложение с критической секцией в 200 раз дороже, чем простое сложение, атомарное сложение в 25 раз дороже, чем простое сложение.

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

Ограничения atomic важные. Они должны быть подробно описаны в спецификациях OpenMP. MSDN предлагает быстрый шпаргалку, так как я не удивлюсь, если это не изменится. (Visual Studio 2012 имеет реализацию OpenMP с марта 2002 года.) Чтобы процитировать MSDN:

Оператор выражения должен иметь одну из следующих форм:

xbinop=expr

x++

++x

x--

--x

В предыдущих выражениях: x является lvalue выражение со скалярным типом. expr является выражением со скалярным типом, и оно не ссылается на объект, обозначенный x, binop не перегружен оператор и является одним из +, *, -, /, &, ^, |, <<, или же >>,

Я рекомендую использовать atomic когда вы можете и назвали критические разделы в противном случае. Называть их важно; таким образом вы избежите головной боли отладки.

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

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

      #pragma omp atomic
a = 5 + fnk();

здесь может вызываться несколькими потоками одновременно, но присваивание a должно быть взаимоисключающим. Как вы можете видеть ниже, вызов fnk() прерывается другим потоком, и мы получили результат 0 2 2 и 0 соответственно. Этого бы не произошло, если бы мы использовали критическое предложение.

Уже отличные объяснения здесь. Тем не менее, мы можем погрузиться немного глубже. Чтобы понять основное различие между концепциями атомарного и критического разделов в OpenMP, мы должны сначала понять концепцию блокировки. Давайте рассмотрим, почему нам нужно использовать блокировки.

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

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

1. Define a variable called lock.
2. For each thread:
   2.1. Read the lock.
   2.2. If lock == 0, lock = 1 and goto 3    // Try to grab the lock
       Else goto 2.1    // Wait until the lock is released
3. Do something...
4. lock = 0    // Release the lock

Данный алгоритм может быть реализован на аппаратном языке следующим образом. Мы будем использовать один процессор и проанализируем поведение блокировок в этом. Для этой практики давайте предположим один из следующих процессоров: MIPS, Alpha, ARM или Power.

try:    LW R1, lock
        BNEZ R1, try
        ADDI R1, R1, #1
        SW R1, lock

Эта программа, кажется, в порядке, но это не так. Приведенный выше код страдает от предыдущей проблемы; синхронизация Давайте найдем проблему. Предположим, что начальное значение блокировки равно нулю. Если два потока запускают этот код, один может достигнуть SW R1, заблокировать, прежде чем другой прочитает переменную блокировки. Таким образом, они оба думают, что замок свободен. Чтобы решить эту проблему, есть другая инструкция, а не простая LW и SW. Это называется инструкциями Read-Modify-Write. Это сложная инструкция (состоящая из подинструкций), которая гарантирует, что процедура получения блокировки выполняется только одним потоком за раз. Отличие Read-Modify-Write по сравнению с простыми инструкциями Read и Write заключается в том, что он использует другой способ загрузки и хранения. Он использует LL(Load Linked) для загрузки переменной блокировки и SC(Store Conditional) для записи в переменную блокировки. Дополнительный Регистр Связи используется, чтобы гарантировать, что процедура получения блокировки выполняется одним потоком. Алгоритм приведен ниже.

1. Define a variable called lock.
2. For each thread:
   2.1. Read the lock and put the address of lock variable inside the Link Register.
   2.2. If (lock == 0) and (&lock == Link Register), lock = 1 and reset the Link Register then goto 3    // Try to grab the lock
       Else goto 2.1    // Wait until the lock is released
3. Do something...
4. lock = 0    // Release the lock

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

Основное различие между критическим и атомным исходит из идеи, что:

Зачем использовать блокировки (новую переменную), в то время как мы можем использовать фактическую переменную (над которой мы выполняем операцию), в качестве переменной блокировки?

Использование новой переменной для блокировок приведет к критическому разделу, в то время как использование фактической переменной в качестве блокировки приведет к атомарной концепции. Критическая секция полезна, когда мы выполняем много вычислений (более одной строки) для фактической переменной. Это связано с тем, что если результат этих вычислений не может быть записан в фактическую переменную, вся процедура должна быть повторена для вычисления результатов. Это может привести к низкой производительности по сравнению с ожиданием снятия блокировки перед входом в область с высокой вычислительной мощностью. Таким образом, рекомендуется использовать атомарную директиву всякий раз, когда вы хотите выполнить одно вычисление (x++, x--, ++x, --x и т. Д.) И использовать критическую директиву, когда более сложная вычислительная область выполняется интенсивный раздел.

atomic - критический раздел с одним оператором, т.е. вы блокируете выполнение одного оператора

критическая секция - это блокировка блока кода

Хороший компилятор переведет ваш второй код так же, как первый

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

Другие вопросы по тегам