В MSVC, почему InterlockedOr и InterlockedAnd генерируют цикл вместо простой заблокированной инструкции?
На MSVC для x64 (19.10.25019),
InterlockedOr(&g, 1)
генерирует эту кодовую последовательность:
prefetchw BYTE PTR ?g@@3JC
mov eax, DWORD PTR ?g@@3JC ; g
npad 3
$LL3@f:
mov ecx, eax
or ecx, 1
lock cmpxchg DWORD PTR ?g@@3JC, ecx ; g
jne SHORT $LL3@f
Я бы ожидал гораздо проще (и без петель):
mov eax, 1
lock or [?g@@3JC], eax
InterlockedAnd
генерирует аналогичный код InterlockedOr
,
Кажется дико неэффективным иметь цикл для этой инструкции. Почему этот код генерируется?
(В качестве примечания: вся причина, по которой я использовал InterlockedOr
должен был сделать атомную загрузку переменной - с тех пор я узнал, что InterlockedCompareExchange
это способ сделать это. Мне странно, что нет InterlockedLoad(&x)
но я отвлекся...)
1 ответ
Документально оформленный контракт на InterlockedOr
возвращает ли оно исходное значение:
InterlockedOr
Выполняет атомарную операцию ИЛИ на указанном
LONG
ценности. Функция не позволяет нескольким потокам использовать одну и ту же переменную одновременно.LONG __cdecl InterlockedOr( _Inout_ LONG volatile *Destination, _In_ LONG Value );
Параметры:
Направление [в, из]
Указатель на первый операнд. Это значение будет заменено результатом операции.Значение [в]
Второй операнд.Возвращаемое значение
Функция возвращает исходное значение параметра Destination.
Вот почему требуется необычный код, который вы наблюдали. Компилятор не может просто создать OR
инструкция с LOCK
префикс, потому что OR
Инструкция не возвращает предыдущее значение. Вместо этого он должен использовать странный обходной путь с LOCK CMPXCHG
в петле. Фактически, эта, по-видимому, необычная последовательность является стандартным шаблоном для реализации взаимосвязанных операций, когда они изначально не поддерживаются базовым оборудованием: перехватите старое значение, выполните сопоставленное сравнение и обмен с новым значением и продолжайте попытки в цикл до тех пор, пока старое значение этой попытки не станет равным захваченному старому значению.
Как вы заметили, вы видите то же самое с InterlockedAnd
по той же причине: x86 AND
Инструкция не возвращает первоначальное значение, поэтому генератор кода должен использовать общий шаблон, включающий сравнение и обмен, который напрямую поддерживается аппаратным обеспечением.
Обратите внимание, что, по крайней мере, на x86, где InterlockedOr
реализован как встроенный, оптимизатор достаточно умен, чтобы выяснить, используете ли вы возвращаемое значение или нет. Если да, то он использует обходной код, включающий CMPXCHG
, Если вы игнорируете возвращаемое значение, то оно идет вперед и выдает код, используя LOCK OR
так же, как вы ожидаете.
#include <intrin.h>
LONG InterlockedOrWithReturn()
{
LONG val = 42;
return _InterlockedOr(&val, 8);
}
void InterlockedOrWithoutReturn()
{
LONG val = 42;
LONG old = _InterlockedOr(&val, 8);
}
InterlockedOrWithoutReturn, COMDAT PROC
mov DWORD PTR [rsp+8], 42
lock or DWORD PTR [rsp+8], 8
ret 0
InterlockedOrWithoutReturn ENDP
InterlockedOrWithReturn, COMDAT PROC
mov DWORD PTR [rsp+8], 42
prefetchw BYTE PTR [rsp+8]
mov eax, DWORD PTR [rsp+8]
LoopTop:
mov ecx, eax
or ecx, 8
lock cmpxchg DWORD PTR [rsp+8], ecx
jne SHORT LoopTop
ret 0
InterlockedOrWithReturn ENDP
Оптимизатор одинаково умный для InterlockedAnd
и должен быть для другого Interlocked*
функции, а также.
Как подсказывает вам интуиция, LOCK OR
реализация более эффективна, чем LOCK CMPXCHG
в петле. Мало того, что есть расширенный размер кода и накладные расходы зацикливания, но вы рискуете пропустить предсказание ветвления, что может стоить большого количества циклов. В коде, критичном к производительности, если вы не можете полагаться на возвращаемое значение для взаимосвязанных операций, вы можете получить повышение производительности.
Однако то, что вы действительно должны использовать в современном C++, это std::atomic
, что позволяет вам указать желаемую модель / семантику памяти, а затем позволить разработчикам стандартной библиотеки справиться со сложностью.