На 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, но это не имеет отношения к вопросу.
Поскольку блокировка все еще требуется для того, чтобы она была атомарной; даже с memory_order_relaxed
требование увеличения / уменьшения слишком строго, чтобы быть без блокировки.
Вообразите то же самое без замков.
v = 0;
И затем мы создаем 100 потоков, каждый с этой командой:
v++;
А потом вы ждете завершения всех потоков, чего бы вы ожидали? К сожалению, это может быть и не 100. Скажем, значение v = 23 загружается одним потоком, и до создания 24 другой поток также загружает 23, а затем записывает 24 тоже. Таким образом, потоки фактически отрицают друг друга. Это потому, что сам прирост не атомный. Конечно, загрузка, сохранение, добавление могут быть атомарными сами по себе, но приращение состоит из нескольких шагов, поэтому оно не является атомарным.
Но с std :: atomic все операции являются атомарными, независимо от std::memory_order
установка. Вопрос только в том, в каком порядке они произойдут. memory_order_relaxed
все еще гарантирует атомарность, это могло бы просто быть не в порядке относительно всего, что происходит рядом с ним, даже работая с тем же значением.
Сначала для справки рассмотрим нормальное назначение. Он генерирует следующее на 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