начальная , считанная с двумя отдельными mov
инструкциями, является не атомарной, но она не в цикле. @ ответ interjay объясняет, почему это хорошо.
Забавный факт: чтение, выполненное cmpxchg8b
, будет атомарным даже без префикса lock
. (Но этот код действительно использует префикс lock
, чтобы сделать всю операцию RMW атомарной, а не разделять атомарную загрузку и атомарное хранилище.)
Гарантируется, что он является атомарным, поскольку он правильно выровнен (и он помещается на одной строке кэша), а также потому, что Intel разработала спецификацию таким образом, см. Руководство по архитектуре Intel, том 1, 4.4.1:
Операнд слова или двойного слова, который пересекает 4-байтовую границу или
операнд четырех слов, который пересекает 8-байтовую границу
не выровнен и требует двух отдельных циклов шины памяти для доступа.
Том 3А 8.1.1:
Процессор Pentium (и более новые процессоры с тех пор) гарантирует, что
следующие дополнительные операции с памятью всегда будут выполняться
атомно:
• Чтение или запись четырех слов, выровненных по 64-битной
граница
• 16-битный доступ к некэшируемым областям памяти, которые соответствуют
в пределах 32-битной шины данных
Процессоры семейства P6 (и новее
процессоры т.к.) гарантируют, что следующая дополнительная память
операция всегда будет выполняться атомарно:
• Выровнены 16-, 32-,
и 64-битный доступ к кеш-памяти, которая помещается в строку кеша
Таким образом, будучи выровненным, он может быть прочитан за 1 цикл и помещается в одну строку кэша, что делает cmpxchg8b
читаемым атомарным.
Если данные были выровнены неправильно, префикс lock
будет все еще сделать его атомарным, но затраты на производительность будут очень высокими из-за простой блокировки кеша (задержка ответа MESI Отклонять запросы на эту одну строку кэша) больше не будет достаточно.
Код возвращается к 0x8048565
(после загрузки mov
, включая копию и add-1), поскольку v
уже загружен; нет необходимости загружать его снова, поскольку CMPXCHG8B
установит EAX:EDX
на значение в месте назначения, если произойдет сбой:
CMPXCHG8B
Описание для руководства Intel ISA Vol. 2A:
Сравните EDX: EAX с m64. Если равно, установите ZF и загрузите ECX: EBX в m64.
Иначе, очистите ZF и загрузите m64 в EDX: EAX.
Таким образом, коду нужно только увеличить новое возвращаемое значение и повторить попытку.
Если мы посмотрим на это в C-коде, это станет проще:
value = dest; // non-atomic but usually won't tear
while(!CAS8B(&dest,value,value + 1))
{
value = dest; // atomic; part of lock cmpxchg8b
}
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * value = dest
на самом деле от того же чтения, который * 10 6 * * используется для части сравнения). В цикле нет отдельной перезагрузки.
Фактически, C11 atomic_compare_exchange_weak
/ _strong
имеет это встроенное поведение: он обновляет «ожидаемый» операнд.
То же самое делает современная встроенная в GCC __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder)
- она принимает значение expected
для справки.
С устаревшими GCC __sync
встроенными , __sync_val_compare_and_swap
возвращает старое значение val (вместо логического результата swap / not-swap для __sync_bool_compare_and_swap
)