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?
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);