Да, есть потенциально огромные преимущества, особенно для локальных переменных или любой переменной, многократно используемой в одной и той же функции. Переменная atomic<>
не может быть оптимизирована в регистр.
Если вы скомпилировали без оптимизации, код генерации был бы похожим, но компиляция с включенной нормальной оптимизацией может иметь огромные различия. Неоптимизированный код похож на создание каждой переменной volatile
.
Текущие компиляторы также никогда не объединяют несколько операций чтения переменной atomic
в одно, как если бы вы использовали volatile atomic<T>
, потому что это то, чего ожидают люди, и пыль еще не улеглась о том, как разрешить полезную оптимизацию при запрете те, которые вы не хотите. ( Почему компиляторы не объединяют избыточные записи std :: atomic? и Может ли компилятор оптимизировать две атомные нагрузки? ).
Это не хороший пример, но представьте, что проверка логического значения выполняется внутри встроенной функции и что внутри цикла есть что-то еще. (В противном случае вы бы поместили if
вокруг цикла, как обычный человек.)
int sumarr_atomic(int arr[]) {
int sum = 0;
for(int i=0 ; i<10000 ; i++) {
if (atomic_bool.load (std::memory_order_relaxed)) {
sum += arr[i];
}
}
return sum;
}
См. Вывод asm на Godbolt .
Но с неатомарным bool
компилятор может сделать это преобразование за вас, подняв нагрузку, а затем автоматически векторизовать простой цикл суммирования (или вообще не запускать его).
С atomic_bool
не может. С atomic_bool цикл asm во многом похож на исходный код C ++, фактически выполняет тестирование и ветвится на значении переменной внутри каждой итерации цикла. И это, конечно, побеждает автоматическую векторизацию.
(Правила C ++ как-бы позволили компилятору поднять нагрузку, потому что она ослаблена, поэтому он может переупорядочивать с неатомарным доступом. И объединяться, потому что чтение одного и того же значения каждый раз является одним из возможных результатов глобального порядка, который читает одно значение. Но, как я уже сказал, компиляторы этого не делают.)
Циклы по массиву bool
могут автоматически векторизоваться, но не более atomic<bool> []
.
Кроме того, инвертирование логического значения с чем-то вроде b ^= 1;
или b++
может быть обычным RMW, а не атомарным RMW, поэтому для него не нужно использовать lock xor
или lock btc
. (x86 атомарный RMW возможен только при последовательной согласованности и переупорядочении во время выполнения, то есть префикс lock
также является полным барьером памяти.)
Код, который изменяет неатомарное логическое значение, может оптимизировать фактические изменения, например,
void loop() {
for(int i=0 ; i<10000 ; i++) {
regular_bool ^= 1;
}
}
компилируется в asm, который хранит regular_bool
в регистре. К сожалению, он не оптимизируется ни к чему (что могло бы произойти, потому что переключение логического значения четное число раз возвращает его к исходному значению). Но это возможно с более умным компилятором.
loop():
movzx edx, BYTE PTR regular_bool[rip] # load into a register
mov eax, 10000
.L17: # do {
xor edx, 1 # flip the boolean
sub eax, 1
jne .L17 # } while(--i);
mov BYTE PTR regular_bool[rip], dl # store back the result
ret
Даже если записать как atomic_b.store( !atomic_b.load(mo_relaxed), mo_relaxed)
(отдельные атомарные загрузки / хранилища), вы все равно получите сохранение / перезагрузку в цикле, создавая 6-циклическую цепочку зависимостей, переносимых циклом через сохранение / перезагрузку (на процессорах Intel с 5-тактной задержкой пересылки памяти) вместо 1-тактной цепи депозита через регистр.