Выполняет ли одна инструкция ассемблера атомарно?

Сегодня я столкнулся с этим вопросом:

у вас есть код

static int counter = 0;
void worker() {
    for (int i = 1; i <= 10; i++)
        counter++;
}

Если worker будет вызываться из двух разных потоков, какое значение будет counter после завершения обоих из них?

Я знаю, что на самом деле это может быть что угодно. Но мои внутренние мужества говорят мне, что counter++ скорее всего будет переведен в одну инструкцию ассемблера, и если оба потока выполняются на одном ядре, counter будет равен 20.

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

Ответ 1

В частности, для x86, и в отношении вашего примера: counter++, существует несколько способов его компиляции. Самый тривиальный пример:

inc counter

Это означает следующие микрооперации:

  • загрузить counter в скрытый регистр на CPU
  • увеличить регистр
  • сохранить обновленный регистр в counter

Это по существу то же самое, что:

mov eax, counter
inc eax
mov counter, eax

Обратите внимание, что если какой-либо другой агент обновляет counter между загрузкой и хранилищем, он не будет отображаться в counter после магазина. Этот агент может быть другим потоком в одном ядре, другом ядре в одном CPU, другом процессоре в той же системе или даже внешнем агенте, использующем DMA (Direct Memory Access).

Если вы хотите гарантировать, что этот inc является атомарным, используйте префикс lock:

lock inc counter

lock гарантирует, что никто не сможет обновить counter между загрузкой и хранилищем.


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

Ответ 2

Не всегда - на некоторых архитектурах одна инструкция сборки преобразуется в одну инструкцию машинного кода, а на других - нет.

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

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

Ответ 3

Ответ: это зависит!

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

Тем не менее, вопрос сводится к одной машинной инструкции атома?

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

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

Ответ: Это зависит. Внимательно прочитайте инструкцию по эксплуатации устройства. В сомнении, это не так!

Изменить: О, я видел это сейчас, вы также просите счетчика ++. Заявление, "скорее всего, будет переведено", не может быть доверено вообще. Это во многом также зависит от компилятора! Это становится сложнее, когда компилятор делает различные оптимизации.

Ответ 4

  • Операции Increment/Decment для 32-битных или менее целых переменных на одном 32-битном процессоре без технологии Hyper-Threading являются атомарными.
  • На процессоре с технологией Hyper-Threading или в многопроцессорной системе операции инкремента/декремента НЕ гарантированно выполняются атомарно.

Ответ 5

Недопустимый комментарий Nathan: Если я правильно помню свой ассемблер Intel x86, инструкция INC работает только для регистров и не работает непосредственно в ячейках памяти.

Таким образом, счетчик ++ не будет одной инструкцией в ассемблере (просто игнорируя часть после инкремента). Это будет как минимум три инструкции: переменная счетчика нагрузки для регистрации, регистр инкремента, регистр нагрузки обратно к счетчику. И это только для архитектуры x86.

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

Ответ 6

Нет, вы не можете этого допустить. Если это четко не указано в спецификации компилятора. И, кроме того, никто не может гарантировать, что одна инструкция ассемблера действительно атомарна. На практике каждая инструкция ассемблера переводится на число операций микрокода - uops.
Также вопрос о состоянии гонки тесно связан с моделью памяти (когерентность, последовательная, слаженность выпуска и т.д.), для каждого ответа и результата могут быть разные.

Ответ 7

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

Ответ 8

Не может быть фактический ответ на ваш вопрос, но (если это С# или другой язык .NET), если вы хотите, чтобы counter++ действительно был многопоточным атомом, вы могли бы использовать System.Threading.Interlocked.Increment(counter).

См. другие ответы для получения фактической информации о разных способах, почему/как counter++ не может быть атомарным.; -)

Ответ 9

В большинстве случаев нет. Фактически, на x86 вы можете выполнить команду

push [address]

который в C будет выглядеть примерно так:

*stack-- = *address;

Выполняет две передачи памяти в одной команде.

Это практически невозможно сделать за 1 такт, не в последнюю очередь потому, что одна передача памяти также невозможна за один цикл!

Ответ 10

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

Для этой цели существуют барьеры памяти (http://en.wikipedia.org/wiki/Memory_barrier)

Короче говоря, в то время как для Intel (с соответствующими префиксами блокировки) достаточно атомарных инструкций, больше нужно делать на неинтеллектуальных операциях, поскольку ввод/вывод памяти может быть не в том же порядке.

Это известная проблема при переносе "незакрепленных" решений от Intel к другим архитектурам.

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

Ответ 11

Я думаю, что вы получите условие гонки на доступ.

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