I am encountering suspicious behavior with std::atomic_ref::is_lock_free() in Microsoft Visual Studio's C++ compiler (MSVC version 14.38.33130). The method returns true for a very large structure (1024 bytes), which strongly contradicts the expectation that such large types cannot be lock-free on x86-64 architecture. After examining the MSVC standard library source code, I believe I've identified a potential bug in the implementation of is_lock_free().
Minimal Reproducible Example (compile using C++ 20)
#include <atomic>
#include <thread>
#include <iostream>
int main() {
union LargeStruct {
char data[1024];
int value;
void operator+(int value) {
this->value += value;
}
};
LargeStruct largeStruct;
memset(&largeStruct, 0, sizeof(largeStruct));
std::atomic_ref<LargeStruct> atomic_struct(largeStruct);
std::atomic_ref<LargeStruct> atomic_struct2(largeStruct);
static_assert(sizeof(LargeStruct) > 8, "LargeStruct size should be larger than 8 bytes for this test.");
std::cout << "atomic_struct.is_lock_free(): " << atomic_struct.is_lock_free() << std::endl;
std::thread t1([&atomic_struct]() {
for (int i = 0; i < 10000; ++i) {
LargeStruct old_val = atomic_struct.load(std::memory_order_relaxed);
LargeStruct new_val;
do {
new_val = old_val;
new_val.value += 1;
} while (!atomic_struct.compare_exchange_weak(
old_val, new_val,
std::memory_order_release,
std::memory_order_relaxed
));
}
});
std::thread t2([&atomic_struct2]() {
for (int i = 0; i < 10000; ++i) {
LargeStruct old_val = atomic_struct2.load(std::memory_order_relaxed);
LargeStruct new_val;
do {
new_val = old_val;
new_val.value += 1;
} while (!atomic_struct2.compare_exchange_weak(
old_val, new_val,
std::memory_order_release,
std::memory_order_relaxed
));
}
});
t1.join();
t2.join();
std::cout << (largeStruct.value);
return 0;
}
Actual Observed Behavior
Expected Behavior
For a 1024-byte structure, is_lock_free() should return false because hardware cannot natively support atomic operations on such a large type. The implementation should use an internal lock mechanism.
The Suspect Code in MSVC's Implementation
_NODISCARD bool is_lock_free() const noexcept {
#if _ATOMIC_HAS_DCAS
return is_always_lock_free;
#else // ^^^ _ATOMIC_HAS_DCAS / !_ATOMIC_HAS_DCAS vvv
if constexpr (is_always_lock_free) {
return true;
} else {
// SUSPICIOUS: Returns based solely on 16-byte CAS support, ignoring type size!
return __std_atomic_has_cmpxchg16b() != 0;
}
#endif // _ATOMIC_HAS_DCAS
}
The problematic logic is in the else branch. When a type is not always lock-free (is_always_lock_free is false), the function returns the result of __std_atomic_has_cmpxchg16b(), which only checks if the CPU supports 16-byte compare-and-swap instructions.
Yes, this is a bug that's fixed in VS 2022 17.12.
Bug report: https://github.com/microsoft/STL/issues/4728
Fix: https://github.com/microsoft/STL/pull/4729