c++atomicmmapvirtualalloc

Is allocating arrays of atomics using virtual memory system calls safe?


I am developing an in-memory database, and my system needs a large array of std::atomic_int objects that roughly act as locks for database records. Now I would prefer to allocate these locks using VM system calls such as mmap on Unix-like systems and VirtualAlloc on Win32/64. There are several reasons for this, and just one of them is not having to explicitly initialise memory (i.e., memory allocated by the VM syscalls is guaranteed to be zeroed by the OS). So, I would essentially like to do this:

#include <sys/mman.h>
#include <atomic>

// ...
size_t numberOfLocks = ... some large number ...;
std::atomic_int* locks = reinterpret_cast<std::atomic_int*>(mmap(0, numberOfLocks * sizeof(std::atomic_int), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0));
// ... use locks[i].load() or locks[i].store() as with any memory order as appropriate

My main question is whether this code is safe. I'd intuitively expect the code to work on any reasonable platform with a modern compiler: mmap is guaranteed to return memory aligned to the VM page boundary so any alignment requirements of std::atomic_int should be honoured, and the constructor of std::atomic_int does not initialise the value so there is no danger of not invoking the constructor as long reads and writes are implemented in a reasonable way (e.g., using the __atomic_* builtins of GCC and clang).

However, I can imagine that this code is not necessarily safe according to the C++ standard — am I right in thinking that? If that is correct, is there any check so that, if the code successfully compiles on the target platform (i.e., if the implementation of std::atomic_int is what I expect it to be), then everything works as I expect?

Related to that, I expect the following code, where std::atomic_int is not property aligned, to break on x86:

uint8_t* region = reinterpret_cast<uint8_t*>(mmap(...));
std::atomic_int* lock = reinterpret_cast<std::atomic_int*>(region + 1);
lock->store(42, std::memory_order_relaxed);

The reason why I think this should not work is because a reasonable implementation of std::atomic_int::store with std::memory_order_relaxed on x86 is just a normal move, which is guaranteed to be atomic only for word-aligned accesses. If I am right about this, is there anything I can add to the code to safeguard against such situations and possibly detect such issues at compile time?


Solution

  • It is safe as mmap allocates memory suitably aligned for any built-in and SIMD type.

    Make sure you call std::uninitialized_default_construct_n (or your own pre-C++17 equivalent) to satisfy the requirement of C++ standard that the constructor must be called, and std::destroy_n to invoke destructors after use. These calls compile into 0 instructions because the default constructor and destructor of std::atomic<> are trivial (do nothing):

    size_t numberOfLocks = ... some large number ...;
    auto* locks = static_cast<std::atomic_int*>(mmap(0, numberOfLocks * sizeof(std::atomic_int), ...));
    
    // initialize
    std::uninitialized_default_construct_n(locks, numberOfLocks); 
    // ... use ...
    // uninitialize
    std::destroy_n(locks, numberOfLocks);