Почему memory_order_relaxed использует атомарные (с префиксом) инструкции на x86?
На Visual C++ 2013, когда я компилирую следующий код
#include <atomic>
int main()
{
std::atomic<int> v(2);
return v.fetch_add(1, std::memory_order_relaxed);
}
Я получаю обратно следующую сборку на x86:
51 push ecx
B8 02 00 00 00 mov eax,2
8D 0C 24 lea ecx,[esp]
87 01 xchg eax,dword ptr [ecx]
B8 01 00 00 00 mov eax,1
F0 0F C1 01 lock xadd dword ptr [ecx],eax
59 pop ecx
C3 ret
и аналогично на x64:
B8 02 00 00 00 mov eax,2
87 44 24 08 xchg eax,dword ptr [rsp+8]
B8 01 00 00 00 mov eax,1
F0 0F C1 44 24 08 lock xadd dword ptr [rsp+8],eax
C3 ret
Я просто не понимаю: почему происходит расслабленный прирост int
переменная требует lock
префикс?
Есть ли причина для этого, или они просто не включают оптимизацию удаления?
* Я использовал /O2
с /NoDefaultLib
урезать его и избавиться от ненужного кода времени выполнения C, но это не имеет отношения к вопросу.
2 ответа
Поскольку блокировка все еще требуется для того, чтобы она была атомарной; даже с memory_order_relaxed
требование увеличения / уменьшения слишком строго, чтобы быть без блокировки.
Вообразите то же самое без замков.
v = 0;
И тогда мы создаем 100 потоков, каждый с этой командой:
v++;
А потом вы ждете завершения всех потоков, чего бы вы ожидали от v? К сожалению, это может быть и не 100. Скажем, значение v=23 загружается одним потоком, и перед созданием 24 другой поток также загружает 23, а затем записывает также 24. Таким образом, потоки фактически отрицают друг друга. Это потому, что сам прирост не атомарный. Конечно, загрузка, сохранение, добавление могут быть атомарными сами по себе, но приращение - это несколько шагов, поэтому оно не атомарно.
Но с std::atomic все операции являются атомарными, независимо от std::memory_order
установка. Вопрос только в том, в каком порядке они произойдут. memory_order_relaxed
все еще гарантирует атомарность, он может быть просто не в порядке относительно всего, что происходит рядом с ним, даже работая с тем же значением.
Атомарные операции, даже с ослабленным упорядочением, все равно должны быть атомарными.
Даже если некоторые операции на текущих процессорах были атомарными безlock
префикс (подсказка: их нет из-за многоядерных кешей), который не будет гарантирован для будущих процессоров.
Было бы недальновидно, если бы все ваши двоичные файлы ужасно вышли из строя на новейшей архитектуре только потому, что вы хотели оптимизировать байт из своего двоичного файла, полагаясь на функцию, которая не является частью спецификации сборки (и, следовательно, не гарантируется сохранение в будущем x86_64 архитектуры)
Конечно, в этом случае широко распространены многоядерные системы, поэтому на практике вам понадобится lock
префикс, чтобы он работал на текущих процессорах. См. Может ли num++ быть атомарным для int num?
Сначала для справки рассмотрим нормальное назначение. Он генерирует следующее на Intel/64:
// v = 10;
000000014000E0D0 mov eax,0Ah
000000014000E0D5 xchg eax,dword ptr [v (014001BCDCh)]
Затем рассмотрите непринужденное задание:
// v.store(10, std::memory_order_relaxed);
000000014000E0D0 mov dword ptr [v (014001BCDCh)],0Ah
Сейчас, std::atomic::fetch_add()
является операцией чтения-изменения-записи, и нет смысла делать это "грязным" способом. По умолчанию вы получаете std::memory_order_seq_cst
согласно http://en.cppreference.com/w/cpp/atomic/atomic/fetch_add. Так что, я думаю, имеет смысл сгенерировать одну нативную инструкцию для этого. По крайней мере, на Intel / 64, где это дешево:
// v.fetch_add(1, std::memory_order_relaxed)
000000014000E0D0 mov eax,1
000000014000E0D5 lock xadd dword ptr [v (014001BCDCh)],eax
В конце концов, вы можете достичь того, чего хотите, явно написав две операции, которые компилятор должен будет выполнить:
// auto x = v.load(std::memory_order_relaxed);
000000014000E0D0 mov eax,dword ptr [v (014001BCDCh)]
// ++x;
000000014000E0D6 inc eax
//v.store(x, std::memory_order_relaxed);
000000014000E0D8 mov dword ptr [v (014001BCDCh)],eax