В 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, что позволяет вам указать желаемую модель / семантику памяти, а затем позволить разработчикам стандартной библиотеки справиться со сложностью.

Другие вопросы по тегам