Введение
Недавно я столкнулся с некоторыми проблемами синхронизации, которые привели меня к spinlocks и атомным счетчикам. Затем я искал немного больше, как они работают, и нашел std:: memory_order и барьеры памяти (mfence
, lfence
и sfence
).
Итак, кажется, что я использую приобретать/выпускать для спин-локов и расслабленной для счетчиков.
Некоторая ссылка
x86 MFENCE - Забор памяти
x86 LOCK - Сигнал подтверждения LOCK #
Вопрос
Что такое машинный код (отредактируйте: см. ниже) для этих трех операций (lock = test_and_set, unlock = clear, increment = operator ++= fetch_add) со значением по умолчанию (seq_cst) и с приобретением/выпуском/расслаблением (в этом порядке для этих трех операций). Какая разница (какие барьеры памяти где) и стоимость (сколько циклов процессора)?
Цель
Мне просто интересно, насколько плохо мой старый код (не указывая порядок памяти = seq_cst), и если я должен создать некоторый class atomic_counter
, полученный из std::atomic
, но используя расслабленное упорядочение памяти ( а также хорошую спин-блокировку с приобретением/выпуском вместо мьютексов в некоторых местах... или использовать что-то из ускорительной библиотеки - я до сих пор избегал повышения).
Мои знания
До сих пор я понимаю, что spinlocks защищает больше, чем он сам (но также и общий ресурс/память), поэтому должно быть что-то, что делает некоторый вид памяти согласованным для нескольких потоков/ядер (это будут те, которые приобретают/выпускают и заборы памяти). Атомный счетчик просто живет для себя и нуждается только в этом атомарном приращении (никакой другой памяти не задействован, и я действительно не забочусь о значении, когда я его читаю, он информативен и может быть несколько циклов старый, без проблем). Существует префикс LOCK
, и некоторые инструкции, такие как xchg
, неявно имеют его. Здесь мои знания заканчиваются, я не знаю, как работают кеш и шины, и что стоит (но я знаю, что современные процессоры могут изменять порядок инструкций, выполнять их параллельно и использовать кеш памяти и некоторую синхронизацию). Спасибо за объяснение.
PS: У меня есть старый 32-битный компьютер, теперь можно видеть только lock addl
и простой xchg
, ничего другого - все версии выглядят одинаково (кроме разблокировки), memory_order не имеет никакого значения для моего старый компьютер (кроме разблокировки, релиз использует move
вместо xchg
). Это будет верно для 64-битного ПК? (изменить: см. ниже) Должен ли я заботиться о порядке памяти? (ответ: нет, не много, релиз на разблокировке сохраняет несколько циклов, все).
Код:
#include <atomic>
using namespace std;
atomic_flag spinlock;
atomic<int> counter;
void inc1() {
counter++;
}
void inc2() {
counter.fetch_add(1, memory_order_relaxed);
}
void lock1() {
while(spinlock.test_and_set()) ;
}
void lock2() {
while(spinlock.test_and_set(memory_order_acquire)) ;
}
void unlock1() {
spinlock.clear();
}
void unlock2() {
spinlock.clear(memory_order_release);
}
int main() {
inc1();
inc2();
lock1();
unlock1();
lock2();
unlock2();
}
g++ -std = С++ 11 -O1 -S ( 32-разрядный Cygwin, сокращенный вывод)
__Z4inc1v:
__Z4inc2v:
lock addl $1, _counter ; both seq_cst and relaxed
ret
__Z5lock1v:
__Z5lock2v:
movl $1, %edx
L5:
movl %edx, %eax
xchgb _spinlock, %al ; both seq_cst and acquire
testb %al, %al
jne L5
rep ret
__Z7unlock1v:
movl $0, %eax
xchgb _spinlock, %al ; seq_cst
ret
__Z7unlock2v:
movb $0, _spinlock ; release
ret
ОБНОВЛЕНИЕ для x86_64bit: (см. mfence
в unlock1
)
_Z4inc1v:
_Z4inc2v:
lock addl $1, counter(%rip) ; both seq_cst and relaxed
ret
_Z5lock1v:
_Z5lock2v:
movl $1, %edx
.L5:
movl %edx, %eax
xchgb spinlock(%rip), %al ; both seq_cst and acquire
testb %al, %al
jne .L5
ret
_Z7unlock1v:
movb $0, spinlock(%rip)
mfence ; seq_cst
ret
_Z7unlock2v:
movb $0, spinlock(%rip) ; release
ret