Предупреждение C26837
Значение для функции сравнения
comp
func
было загружено из расположения назначенияdest
через ненезависимое чтение.
Это правило было добавлено в Visual Studio 2022 17.8.
Замечания
Функция InterlockedCompareExchange
и ее производные, например InterlockedCompareExchangePointer
, выполняют атомарную операцию сравнения и обмена по указанным значениям. Destination
Если значение равно Comparand
значению, обменное значение хранится в адресе, указанном в параметреDestination
. В противном случае операция не выполняется. Функции interlocked
предоставляют простой механизм синхронизации доступа к переменной, общей несколькими потоками. Эта функция является атомарной в отношении вызовов других interlocked
функций. Неправильное использование этих функций может создавать код объекта, который ведет себя не так, как ожидается, так как оптимизация может изменить поведение кода непредвиденными способами.
Рассмотрим следующий код:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Цель этого кода :
- Считывает текущее значение из указателя
plock
. - Проверьте, имеет ли это текущее значение наименьшее количество битов.
- Если он имеет наименьший набор битов, снимите бит при сохранении других битов текущего значения.
Для этого копия текущего значения считывается из plock
указателя и сохраняется в переменной lock
стека. lock
используется три раза:
- Во-первых, чтобы проверить, установлен ли наименьший бит.
- Во-вторых, в качестве
Comparand
значенияInterlockedCompareExchange64
. - Наконец, в сравнении возвращаемого значения из
InterlockedCompareExchange64
В этом случае предполагается, что текущее значение, сохраненное в переменной стека, считывается сразу в начале функции и не изменяется. Это необходимо, так как текущее значение сначала проверяется перед попыткой операции, а затем явно используется в качестве Comparand
входного InterlockedCompareExchange64
и, наконец, используется для сравнения возвращаемого значения.InterlockedCompareExchange64
К сожалению, предыдущий код можно скомпилировать в сборку, которая ведет себя не так, как ожидалось от исходного кода. Скомпилируйте предыдущий код с помощью компилятора Microsoft Visual C++ (MSVC) и /O1
проверьте результирующий код сборки, чтобы узнать, как получается значение блокировки для каждой ссылки lock
. Версия компилятора MSVC версии 19.37 создает код сборки, который выглядит следующим образом:
plock$ = 8
bool TryLock(__int64 *) PROC ; TryLock, COMDAT
mov r8b, 1
test BYTE PTR [rcx], r8b
je SHORT $LN3@TryLock
mov rdx, QWORD PTR [rcx]
mov rax, QWORD PTR [rcx]
and rdx, -2
lock cmpxchg QWORD PTR [rcx], rdx
je SHORT $LN4@TryLock
$LN3@TryLock:
xor r8b, r8b
$LN4@TryLock:
mov al, r8b
ret 0
bool TryLock(__int64 *) ENDP ; TryLock
rcx
содержит значение параметра plock
. Вместо того, чтобы создать копию текущего значения в стеке, код сборки повторно считывает значение каждый plock
раз. Это означает, что значение может отличаться при каждом чтении. Это делает недействительным очистку, выполняемую разработчиком. Значение повторно считывается после plock
проверки того, что он имеет наименьший битовый набор. Так как он выполняется после выполнения этой проверки, новое значение больше не может иметь наименьший битовый набор. В условиях гонки этот код может вести себя так, как если бы он успешно получил указанную блокировку, когда он уже был заблокирован другим потоком.
Компилятор может удалять или добавлять операции чтения или записи памяти, если поведение кода не изменяется. Чтобы компилятор не внося такие изменения, принудительно считывает значение из памяти и кэширует volatile
его в переменной. Объекты, объявленные как не используемые в определенных оптимизациях, так как volatile
их значения могут изменяться в любое время. Созданный код всегда считывает текущее значение volatile
объекта при запросе, даже если предыдущая инструкция запрашивает значение из того же объекта. Обратный вариант также применяется по той же причине. Значение volatile
объекта не считывается снова, если запрашивается. Дополнительные сведения о volatile
см. в разделе volatile
. Например:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *static_cast<volatile __int64*>(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Скомпилируйте этот код с тем же /O1
параметром, что и раньше. Созданная сборка больше не считывается plock
для использования кэшированного значения в lock
.
Дополнительные примеры исправления кода см. в разделе "Пример".
Имя анализа кода: INTERLOCKED_COMPARE_EXCHANGE_MISUSE
Пример
Компилятор может оптимизировать следующий код для многократного чтения plock
вместо использования кэшированного значения в lock
:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Чтобы устранить проблему, принудительно выполняется volatile
чтение, чтобы компилятор не оптимизирул код для последовательного чтения из той же памяти, если явно не указано. Это запрещает оптимизатору вводить непредвиденное поведение.
Первый метод обработки памяти так же, как volatile
и для принятия адреса назначения в качестве volatile
указателя:
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Второй метод использует чтение volatile
из адреса назначения. Это можно сделать несколькими способами.
- Приведение указателя к
volatile
указателю перед разыменовыванием указателя - Создание указателя
volatile
из предоставленного указателя - Использование
volatile
вспомогательных функций чтения.
Например:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = ReadNoFence64(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
Эвристика
Это правило применяется путем определения того, загружается ли значение в Destination
InterlockedCompareExchange
функции или любой из производных функций через нечитаемыйvolatile
, а затем используется в качестве Comparand
значения. Однако он не проверяет, используется ли загруженное значение для определения значения обмена . Предполагается, что значение обмена связано со значением Comparand
.
См. также
InterlockedCompareExchange
function (winnt.h)
_InterlockedCompareExchange
встроенные функции