C / C ++: расслабленный std :: atomic <bool>против разблокированного bool на архитектуре X64 - PullRequest
0 голосов
/ 11 ноября 2018

Есть ли какое-либо преимущество по эффективности при использовании разблокированного логического значения по сравнению с std::atomic<bool>, где операции всегда выполняются с ослабленным порядком памяти? Я хотел бы предположить, что оба в конечном итоге компилируются в один и тот же машинный код, поскольку на аппаратном обеспечении X64 один байт фактически является атомарным. Я не прав?

Ответы [ 2 ]

0 голосов
/ 11 ноября 2018

Да, есть потенциально огромные преимущества, особенно для локальных переменных или любой переменной, многократно используемой в одной и той же функции. Переменная 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-тактной цепи депозита через регистр.

0 голосов
/ 11 ноября 2018

Проверка при Godbolt , загрузка обычного bool и std::atomic<bool> генерирует другой код, но не из-за проблем с синхронизацией. Вместо этого компилятор (gcc), похоже, не желает предполагать, что std::atomic<bool> гарантированно будет 0 или 1. Странно, что.

Clang делает то же самое, хотя сгенерированный код немного отличается в деталях.

...