c++visual-c++visual-studio-2022lock-freestdatomic

MSVC 14.38.33130: Does std::atomic_ref::is_lock_free have a bug? It returns true for a 1024-byte struct


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

  1. For a 1024-byte LargeStruct, is_lock_free() returns 1 (true), which is the core of the bug.
  2. The program completes successfully and outputs the correct final value of 20000, proving that the atomic operations are working correctly from a functional perspective (likely using an internal lock).

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.


Solution

  • 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