Почему 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  
Другие вопросы по тегам