Польза цикла на основе сравнения от паузы? - PullRequest
0 голосов
/ 07 ноября 2018

В цикле на основе CAS, например, приведенном ниже, выгодно ли использование паузы на x86?

void atomicLeftShift(atomic<int>& var, int shiftBy)
{
    While(true) {
        int oldVal = var;
        int newVal = oldVal << shiftBy;
         if(var.compare_exchange_weak(oldVal, newVal));
             break;
        else
            _mm_pause();
    }
}

1 Ответ

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

Нет, я так не думаю. Это не ожидание вращения. Он не ждет, пока другой поток сохранит 0 или что-то еще. имеет смысл иметь смысл попробовать снова сразу после сбоя lock cmpxchg, а не в режиме сна ~ 100 циклов (на Skylake и более поздних) или ~ 5 циклов (на более ранних процессорах Intel).

Если lock cmpxchg завершить вообще (успешно или неудачно), это означает, что строка кэша теперь находится в состоянии Modified (или, может быть, просто исключено?) В этом ядре, поэтому прямо сейчас самое время попробовать еще раз.

Реальные сценарии использования атомных соединений без блокировки обычно не чрезвычайно тяжело утверждаются, иначе вы должны обычно использовать запасной вариант для сна / пробуждения с помощью ОС.

(Но если есть конкуренция, есть аппаратный арбитраж для lock инструкции ред, в высшей степени утверждали случае, если я не знаю, если это, вероятно, для ядра, чтобы выполнить 2-й * инструкцию 1023 * ред до потери снова строка кэша. Но, надеюсь, да.)

lock cmpxchg не может внезапно потерпеть неудачу, поэтому фактическая живая блокировка невозможна: по крайней мере одно ядро ​​добьется прогресса, если его CAS преуспеет в алгоритме, подобном этому, для раунда каждого ядра, имеющего ход. В архитектуре LL / SC compare_exchange_weak может внезапно давать сбой, поэтому переносимость на не-x86 может потребовать заботы о livelock, в зависимости от деталей реализации, но я думаю, что это маловероятно. (И, конечно, _mm_pause, в частности, только x86.)


Другая причина использования pause состоит в том, чтобы избежать ошибочных предположений порядка памяти при выходе из цикла ожидания с вращением, который вращается только для чтения, ожидая, пока замок не будет разблокирован, прежде чем пытаться атомарно заявить о нем. (Это лучше, чем вращаться на xchg или lock cmpxchg и иметь все ожидающие потоки, забивающие строку кэша.)

Но это опять-таки не проблема, потому что цикл повторов уже включает lock cmpxchg, который является полным барьером, а также атомарный RMW, поэтому я думаю, что это позволяет избежать ошибочных предположений порядка памяти.


Особенно, если вы пишете цикл эффективно / правильно для использования результата загрузки сбоя cmpxchg при повторных попытках, удаляя чистую нагрузку var из цикла .

Это канонический способ построения произвольной атомарной операции из примитива CAS. compare_exchange_weak обновляет свой первый аргумент, если сравнение не удается, поэтому вам не нужна другая загрузка внутри цикла.

#include <atomic>

int atomicLeftShift(std::atomic<int>& var, int shiftBy)
{
    int expected = var.load(std::memory_order_relaxed);
    int desired;
    do {
        desired = expected << shiftBy;
    } while( !var.compare_exchange_weak(expected, desired) );  // seq_cst
    return desired;
}

компилируется с clang7.0 -O3 для x86-64 в этот ассемблер в проводнике компилятора Godbolt:

atomicLeftShift(std::atomic<int>&, int):      
    mov     ecx, esi
    mov     eax, dword ptr [rdi]        # pure load outside the loop

.LBB0_1:                              # do {
    mov     edx, eax
    shl     edx, cl                     # desired = expected << count
    lock            cmpxchg dword ptr [rdi], edx   # eax = implicit expected, updated on failure
    jne     .LBB0_1                   # } while(!CAS)
    mov     eax, edx                  # return value
    ret

Единственный доступ к памяти в повторном цикле - lock cmpxchg, который не может пострадать из-за неправильного предположения порядка памяти. По этой причине pause не требуется.


Также не требуется pause для простой задержки отката, кроме случаев, когда у вас много споров и вы хотите, чтобы один поток делал несколько вещей подряд с одной и той же общей переменной для увеличения пропускной способности. то есть отключить другие потоки в том редком случае, когда cmpxchg терпит неудачу.

Это only имеет смысл, если нормально, когда один поток выполняет несколько атомарных операций в строке над одной и той же переменной (или одной в той же строке кэша, если у вас есть проблемы с ложным разделением), вместо помещая больше операций в одну CAS-попытку.

Это, вероятно, редко встречается в реальном коде, но часто встречается в синтетическом микробенчмарке, где вы позволяете нескольким потокам многократно отбрасывать общую переменную без какой-либо другой работы между ними.

...