Является ли приращение целочисленным атомом в x86?

На многоядерной машине x86, скажем, поток, выполняющийся на ядре 1, увеличивает целочисленную переменную a в то же время, когда поток на ядре 2 также увеличивает ее. Учитывая, что начальное значение a было 0, всегда ли оно будет 2 в конце? Или это может иметь какое-то другое значение? Предположим, что a объявлено как volatile, и мы не используем атомарные переменные (такие как atomic‹> C++ и встроенные атомарные операции в gcc).

Если значение a действительно всегда будет равно 2 в таком случае, означает ли это, что long int в x86-64 также будет иметь то же свойство, то есть a всегда будет 2 в конце?


person pythonic    schedule 08.05.2012    source источник
comment
если вы не используете специальный атомарный тип, приращение обычно представляет собой три отдельные операции. Загрузить, увеличить, затем сохранить.   -  person Hunter McMillen    schedule 08.05.2012
comment
volatile не дает вам атомарного доступа.   -  person Cat Plus Plus    schedule 08.05.2012
comment
@CatPlusPlus твоё имя — атомарная операция? :П   -  person MByD    schedule 08.05.2012
comment
доступ к одному и тому же типу приведет к состоянию гонки, и если не позаботиться должным образом, значение будет неожиданным. здесь, в этом случае, скажем, поток 1 и поток 2 загружают значение одновременно и делают его равным 2, поэтому независимо от того, будет ли поток 1 или поток 2, кто бы ни загружал его первым, окончательное значение будет равно 2   -  person Invictus    schedule 08.05.2012
comment
@CatPlusPlus: существует расширение для компилятора MS, которое делает volatile-переменные атомарными при записи (которые являются атомарными потоками). Я не думаю, что это распространяется на g++. Также я не на 100% знаком с функциональностью, так как не использую. См. msdn.microsoft.com/en-us/library/12a04hfd.aspx< /а>   -  person Martin York    schedule 08.05.2012
comment
Чтение и запись являются атомарными до тех пор, пока вы не пересекаете границу строки кэша. Выполнение обоих с другой операцией между ними не автоматически атомарно.   -  person Mark Ransom    schedule 08.05.2012
comment
@MarkRansom Может быть. В зависимости от того, как вы определяете atomic. Самое последнее использование (в том числе в стандарте C++) придает ему более строгое значение, так что без специальных дополнительных инструкций никакие операции записи или чтения не являются атомарными.   -  person James Kanze    schedule 08.05.2012


Ответы (4)


Машинная инструкция увеличения памяти на X86 является атомарной, только если вы используете ее с префиксом LOCK.

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

Я считаю, что С++ 11 предлагает атомарные приращения, и у большинства компиляторов поставщиков есть идиоматический способ вызвать атомарное приращение определенных встроенных целочисленных типов (обычно int и long); см. справочное руководство по вашему компилятору.

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

Обратите внимание, что вам также нужно беспокоиться об атомарном чтении. На x86 чтение 32- или 64-битного значения оказывается атомарным, если оно выровнено по 64-битному слову. Это не будет верно для «большого значения»; снова вам понадобится стандартный замок.

person Ira Baxter    schedule 08.05.2012
comment
Обеспечивает ли префикс LOCK также необходимые ограждения? (Я так думаю, но не уверен.) - person James Kanze; 08.05.2012

Вот одно доказательство того, что в конкретной реализации (gcc) это не атомарно. Как видите (?), gcc генерирует код, который

  1. загружает значение из памяти в регистр
  2. увеличивает содержимое регистра
  3. сохраняет регистр обратно в память.

Это очень далеко от атомарности.

$ cat t.c
volatile int a;

void func(void)
{
    a++;
}
[19:51:52 0 ~] $ gcc -O2 -c t.c
[19:51:55 0 ~] $ objdump -d t.o

t.o:     file format elf32-i386


Disassembly of section .text:

00000000 <func>:
   0:   a1 00 00 00 00          mov    0x0,%eax
   5:   83 c0 01                add    $0x1,%eax
   8:   a3 00 00 00 00          mov    %eax,0x0
   d:   c3                      ret

Не дайте себя обмануть 0x0 в инструкции mov, там есть место для 4 байтов, и компоновщик заполнит результирующий адрес памяти для a при линковке этого объектного файла.

person nos    schedule 08.05.2012
comment
Забавно, вы на самом деле получаете отдельную загрузку/добавление/сохранение только тогда, когда a равно volatile, иначе gcc использует чтение-изменение-запись (если не настроено для i586 (pentium)). Конечно, если окружающий код использует значение num++, скорее всего, это будет сделано с помощью отдельных инструкций, чтобы оставить результат в регистре. Я упомянул об этом в своем ответе на не-3_ версию вопроса . - person Peter Cordes; 12.10.2016
comment
@PaterCordes Без volatile gcc может сгенерировать одну инструкцию addl , хотя она также не является атомарной, если вы также не используете префикс lock. - person nos; 12.10.2016
comment
:P Вторая ссылка в моем предыдущем комментарии относится к моему ответу, который точно объясняет (с точки зрения протокола MESI и т. д.), почему addl $1, num не является атомарным (за исключением однопроцессорной системы, если мы не включаем наблюдателей DMA), и почему именно с lock. - person Peter Cordes; 12.10.2016

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

Поток 1 загружает значение 0

Поток 2 загружает значение 0

Поток 1 увеличивает и сохраняет 1

Поток 2 увеличивает свою копию значения локального регистра и сохраняет 1.

Как видите, конечным результатом является значение, равное 1, а не 2. В конце не всегда будет 2.

person Michael Dorgan    schedule 08.05.2012

Это не гарантируется. Вы можете использовать инструкцию lock xadd для достижения того же эффекта, или использовать C++ std::atomic, или использовать #pragma omp atomic, или любое количество других решений для параллелизма, которые были написаны, чтобы избавить вас от необходимости заново изобретать колесо.

person Jon Purdy    schedule 08.05.2012