Занятый опрос std::atomic - msvc оптимизирует петлю - почему и как предотвратить?

Я пытаюсь реализовать простую функцию цикла занятости.

Это должно продолжать опрашивать переменную std::atomic максимальное количество раз (spinCount) и возвращать true, если статус действительно изменился (на что-либо, кроме NOT_AVAILABLE) в течение заданных попыток, или false в противном случае:

// noinline is just to be able to inspect the resulting ASM a bit easier - in final code, this function SHOULD be inlined!
__declspec(noinline) static bool trySpinWait(std::atomic<Status>* statusPtr, const int spinCount)
{
    int iSpinCount = 0;
    while (++iSpinCount < spinCount && statusPtr->load() == Status::NOT_AVAILABLE);
    return iSpinCount == spinCount;
}

Однако кажется, что MSVC просто оптимизирует петлю в режиме Release для Win64. Я довольно плохо разбираюсь в сборке, но мне не кажется, что она когда-либо даже пытается прочитать значение statusPtr:

int iSpinCount = 0;
000000013F7E2040  xor         eax,eax  
    while (++iSpinCount < spinCount && statusPtr->load() == Status::NOT_AVAILABLE);
000000013F7E2042  inc         eax  
000000013F7E2044  cmp         eax,edx  
000000013F7E2046  jge         trySpinWait+12h (013F7E2052h)  
000000013F7E2048  mov         r8d,dword ptr [rcx]  
000000013F7E204B  test        r8d,r8d  
000000013F7E204E  je          trySpinWait+2h (013F7E2042h)  
    return iSpinCount == spinCount;
000000013F7E2050  cmp         eax,edx  
000000013F7E2052  sete        al  

У меня сложилось впечатление, что std::atomic с std::memory_order_sequential_cst создает барьер компилятора, который должен предотвратить что-то подобное, но, похоже, это не так (вернее, мое понимание, вероятно, было неверным).

Что я здесь делаю неправильно, или, скорее, как мне лучше всего реализовать этот цикл, не оптимизируя его, с наименьшим влиянием на общую производительность?

Я знаю, что мог бы использовать #pragma optim( "", off ), но (кроме приведенного выше примера) в моем окончательном коде я бы очень хотел, чтобы этот вызов был встроен в более крупную функцию по соображениям производительности. кажется, что эта #pragma обычно предотвращает встраивание.

Цените любые мысли!

Спасибо


person Bogey    schedule 29.11.2018    source источник


Ответы (1)


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

Он перезагружает его на каждой итерации цикла:

000000013F7E2048  mov         r8d,dword ptr [rcx] # rcx is statusPtr

У меня сложилось впечатление, что std::atomic с std::memory_order_sequential_cst создает барьер компилятора, который должен предотвратить что-то подобное,

Вам не нужно ничего больше, чем std::memory_order_relaxed здесь, потому что есть только одна переменная, разделяемая между потоками (более того, этот код не меняет значение атомарной переменной). Проблем с повторным заказом нет.

Другими словами, эта функция работает так, как ожидалось.

Вы можете использовать инструкцию PAUSE, см. Циклы сна мощности и производительности.

person Maxim Egorushkin    schedule 29.11.2018
comment
Спасибо - тогда это очень интересно. Я использую это в контексте шины сообщений. Если я установлю #pragma optim( , off ) только для вышеуказанной функции, все будет работать так, как ожидалось; если я этого не сделаю, это выдаст ошибку (сообщения, поступающие не по порядку или с неправильными - возможно, устаревшими - данными). Кажется слишком странным, чтобы быть совпадением, но, возможно, моя ошибка где-то в другом месте. - person Bogey; 29.11.2018
comment
@Bogey Я подозреваю, что ошибка в другом месте. - person Maxim Egorushkin; 29.11.2018
comment
Так же учтите, что на х86 дальше взаимоисключения (в сборке) не нужно - mov и друзья уже гарантированно атомарны. - person Max Langhof; 29.11.2018
comment
@MaxLanghof В C ++ вам все еще нужно std::atomic, чтобы компилятор не оптимизировал загрузку вне цикла. Раньше std::atomic люди ругали volatile за это. - person Maxim Egorushkin; 29.11.2018
comment
@MaximEgorushkin Поэтому и добавил в сборку. Конечно, вы не можете удалить синхронизацию в коде C++. Я просто хотел уточнить отсутствие операций синхронизации на уровне сборки, которые воспринимал OP. - person Max Langhof; 29.11.2018
comment
@MaxLanghof В этом случае вам придется asm volatile, и вы действительно не хотите туда идти. В общем, просто атомарно. - person Passer By; 29.11.2018
comment
@Bogey: #pragma optimize( "", off ) останавливает встраивание функции? Это, вероятно, важно, потому что не встроенные функции обычно должны рассматриваться как барьер памяти для переупорядочения во время компиляции. (Поскольку неизвестная функция может читать/записывать любые данные, на которые может быть глобальный указатель.) Это может указывать на гонку данных по неатомарным переменным в вызывающем объекте, что позволяет что-то оптимизировать. - person Peter Cordes; 30.11.2018
comment
@MaxLanghof: OP сказал, что, по их мнению, memory_order_sequential_cst будет барьером компилятора, который предотвратит переупорядочение или оптимизацию во время компиляции. Не то чтобы они ожидали увидеть в ассемблере инструкцию барьера времени выполнения mfence. (Но да, атомарные загрузки seq-cst не нуждаются в чем-то более сильном, чем то, что x86 постоянно делает для естественно выровненных загрузок. Но хранилищам seq-cst нужно использовать xchg или mov+mfence, чтобы заблокировать переупорядочивание StoreLoad. Технически это возможно. сделать наоборот, сделать грузы дорогими, а магазины дешевле, но это никому не нужно.) - person Peter Cordes; 30.11.2018
comment
Спасибо @PeterCordes. Теперь я нашел свою ошибку - #pragma прекратила встраивание, но она просто предотвратила ошибку, сделав вызов в целом немного медленнее, что, в свою очередь, вызвало немного другую стратегию опроса, которую я использую, - и ошибка была устранена в этой альтернативной стратегии резервного опроса. Таким образом, приведенный выше код действительно в порядке, проблема была в несвязанной части кода. Спасибо за вклад каждого! - person Bogey; 30.11.2018