xo-alloc2: + guard bytes at beginning of arena + refactoring

This commit is contained in:
Roland Conybeare 2025-12-13 22:15:43 -05:00
commit 0e10777767
8 changed files with 210 additions and 89 deletions

View file

@ -0,0 +1,59 @@
/** @file ArenaConfig.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <string>
#include <cstdint>
namespace xo {
namespace mm {
/** @class ArenaConfig
*
* @brief configuration for a @ref DArena instance
**/
struct ArenaConfig {
/** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/
///@{
/** optional name, for diagnostics **/
std::string name_;
/** desired arena size -- hard max = reserved virtual memory **/
std::size_t size_;
/** hugepage size -- using huge pages relieves some TLB pressure
* (provided you use their full extent :)
**/
std::size_t hugepage_z_ = 2 * 1024 * 1024;
/** if non-zero, allocate extra space between allocs, and fill
* with fixed test-pattern contents. Allows for simple
* runtime arena sanitizing checks.
* Will be rounded up to multiple of @ref padding::c_alloc_alignment
**/
std::size_t guard_z_ = 0;
/** if guard_z_ > 0, write at least that many copies
* of this guard byte following each complete allocation
**/
std::uint8_t guard_byte_ = 0xfd;
/** if store_header_flag_ is true: mask bits for allocation size.
* remaining bits can be stolen for other purposes
* otherwise ignored
**/
/** true to store header (8 bytes) at the beginning of each allocation.
* necessary and sufficient to allows iterating over allocs
* present in arena
**/
bool store_header_flag_ = false;
std::uint64_t header_size_mask_ = 0xffffffff;
/** true to enable debug logging **/
bool debug_flag_ = false;
///@}
};
} /*namespace mm*/
} /*namespace xo*/
/* end ArenaConfig.hpp */

View file

@ -5,49 +5,11 @@
#pragma once
#include <string>
#include "ArenaConfig.hpp"
namespace xo {
namespace mm {
/** @class ArenaConfig
*
* @brief configuration for a @ref DArena instance
**/
struct ArenaConfig {
/** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/
///@{
/** optional name, for diagnostics **/
std::string name_;
/** desired arena size -- hard max = reserved virtual memory **/
std::size_t size_;
/** hugepage size -- using huge pages relieves some TLB pressure
* (provided you use their full extent :)
**/
std::size_t hugepage_z_ = 2 * 1024 * 1024;
/** true to store header (8 bytes) at the beginning of each allocation.
* necessary and sufficient to allows iterating over allocs
* present in arena
**/
bool store_header_flag_ = false;
/** if non-zero, allocate extra space between allocs, and fill
* with fixed test-pattern contents. Allows for simple
* runtime arena sanitizing checks.
* Will be rounded up to multiple of @ref padding::c_alloc_alignment
**/
std::size_t guard_z_ = 0;
/** if store_header_flag_ is true: mask bits for allocation size.
* remaining bits can be stolen for other purposes
* otherwise ignored
**/
std::uint64_t header_size_mask_ = 0xffffffff;
/** true to enable debug logging **/
bool debug_flag_ = false;
///@}
};
/** @class DArena
*
* @brief represent arena allocator state

View file

@ -9,9 +9,7 @@
#include <cassert>
namespace xo {
namespace mm {
struct IAllocator_Any;
}
namespace mm { struct IAllocator_Any; }
namespace facet {
template <>
@ -22,12 +20,14 @@ namespace xo {
namespace mm {
/** @class IAllocator_Any
* @brief Allocator implementation for variant instance.
* @brief Allocator implementation for empty variant instance.
**/
struct IAllocator_Any : public AAllocator {
//using Impl = IAllocator_ImplType<xo::facet::DVariantPlaceholder>;
using size_type = std::size_t;
const AAllocator * iface() const { return std::launder(this); }
// from AAllocator
int32_t _typeseq() const noexcept override { return s_typeseq; }

View file

@ -34,7 +34,7 @@ namespace xo {
enum class alloc_mode : uint8_t {
standard,
super,
sub,
sub_incomplete,
sub_complete,
};
@ -75,10 +75,7 @@ namespace xo {
/** alloc driver. shared by alloc(), super_alloc(), sub_alloc() **/
static value_type _alloc(DArena &,
size_type z,
alloc_mode mode,
bool store_header_flag,
bool remember_header_flag);
alloc_mode mode);
};
// template <>

View file

@ -2,10 +2,13 @@
set(SELF_LIB xo_alloc2)
set(SELF_SRCS
AAllocator.cpp
DArena.cpp
IAllocator_Any.cpp
IAllocator_DArena.cpp
IGCObject_Any.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})

View file

@ -20,7 +20,10 @@ namespace xo {
* e.g. IAllocator_Xfer<DArena>
*/
std::cerr << "fatal: attempt to call uninitialized IAllocator_Any" << std::endl;
std::cerr << "fatal"
<< ": attempt to call uninitialized"
<< " IAllocator_Any method"
<< std::endl;
std::terminate();
}

View file

@ -8,6 +8,7 @@
#include "xo/indentlog/scope.hpp"
#include <cassert>
#include <cstddef>
#include <cstring>
#include <sys/mman.h>
namespace xo {
@ -77,12 +78,6 @@ namespace xo {
s.last_error_ = AllocatorError(error::reserve_exhausted,
s.error_count_,
target_z, s.committed_z_, reserved(s));
#ifdef OBSOLETE
throw std::runtime_error(tostr("ArenaAlloc::expand: requested size exceeds reserved size",
xtag("requested", target_z),
xtag("reserved", reserved(s))));
#endif
return false;
}
@ -114,13 +109,15 @@ namespace xo {
// log && log(xtag("aligned_offset_z", aligned_offset_z),
// xtag("add_commit_z", add_commit_z));
// log && log("expand committed range",
// xtag("commit_start", commit_start),
// xtag("add_commit_z", add_commit_z),
// xtag("commit_end", commit_start + add_commit_z));
if (::mprotect(commit_start, add_commit_z, PROT_READ | PROT_WRITE) != 0) [[unlikely]] {
if (::mprotect(commit_start,
add_commit_z,
PROT_READ | PROT_WRITE) != 0) [[unlikely]]
{
++(s.error_count_);
s.last_error_ = AllocatorError(error::commit_failed,
s.error_count_,
@ -136,11 +133,22 @@ namespace xo {
s.committed_z_ = aligned_target_z;
s.limit_ = s.lo_ + s.committed_z_;
if (commit_start == s.lo_) [[unlikely]]
{
/* first expand() for this allocator - start with guard_z_ bytes */
::memset(s.free_,
s.config_.guard_byte_,
s.config_.guard_z_);
s.free_ += s.config_.guard_z_;
}
assert(s.committed_z_ % s.config_.hugepage_z_ == 0);
assert(reinterpret_cast<size_t>(s.limit_) % s.config_.hugepage_z_ == 0);
return true;
}
} /*expand*/
std::byte *
IAllocator_DArena::alloc(DArena & s,
@ -165,8 +173,6 @@ namespace xo {
* ArenaConfig.store_header_flag_ disabled
*/
bool remember_header_flag = s.config_.store_header_flag_;
return _alloc(s, req_z,
alloc_mode::super);
}
@ -258,17 +264,37 @@ namespace xo {
#endif
}
std::byte *
byte *
IAllocator_DArena::_alloc(DArena & s,
std::size_t req_z,
bool store_header_flag,
bool remember_header_flag)
alloc_mode mode)
{
scope log(XO_DEBUG(s.config_.debug_flag_));
/*
* sub_complete
* sub_incomplete |
* standard super | |
* v v v v
*/
std::array<bool, 4> store_header_v = {{ true, true, false, false }};
std::array<bool, 4> retain_header_v = {{ false, true, false, false }};
std::array<bool, 4> store_guard_v = {{ true, false, false, true }};
/* -> write header at s.free_ */
bool store_header_flag = false;
/* -> stash s.last_header_*/
bool retain_header_flag = false;
/* -> write guard bytes */
bool store_guard = false;
if (s.config_.store_header_flag_) {
store_header_flag = store_header_v[(int)mode];
retain_header_flag = retain_header_v[(int)mode];
store_guard = store_guard_v[(int)mode];
}
assert(padding::is_aligned((size_t)s.free_));
/* remember_header_flag -implies-> store_header_flag */
assert(store_header_flag || !remember_header_flag);
/*
* free_(pre)
@ -277,7 +303,7 @@ namespace xo {
* <-------------z1--------------->
* < guard >< hz >< req_z >< dz >< guard >
*
* used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> unallocated
* used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail
*
* ^ ^ ^
* header mem |
@ -296,12 +322,15 @@ namespace xo {
/* dz: pad req_z to alignment size (multiple of 8 bytes, probably) */
size_t dz = padding::alloc_padding(req_z);
size_t z0 = req_z + dz;
/* if non-zero: will store padded alloc size at the beginning of each allocation
* reminder: important to store padded size for correct arena iteration
/* if non-zero:
* will store padded alloc size at the beginning of each allocation
* reminder:
* important to store padded size for correct arena iteration
*/
uint64_t header = req_z + dz;
if (store_header_flag) {
if (store_header_flag)
{
if ((s.config_.header_size_mask_ & z0) == z0) [[likely]] {
hz = sizeof(header);
} else {
@ -309,7 +338,9 @@ namespace xo {
++(s.error_count_);
s.last_error_ = AllocatorError(error::header_size_mask,
s.error_count_,
0 /*add_commit_z*/, s.committed_z_, reserved(s));
0 /*add_commit_z*/,
s.committed_z_,
reserved(s));
return nullptr;
}
}
@ -318,32 +349,45 @@ namespace xo {
assert(padding::is_aligned(z1));
if (expand(s, allocated(s) + z1)) [[likely]] {
if (store_header_flag) {
(*(uint64_t *)s.free_) = header;
if (remember_header_flag) {
s.last_header_ = (uint64_t *)s.free_;
}
}
byte * mem = s.free_ + hz;
s.free_ += z1;
log && log(xtag("self", s.config_.name_),
xtag("hz", hz),
xtag("z0", req_z),
xtag("+pad", dz),
xtag("z1", z1),
xtag("size", size(s)),
xtag("avail", available(s)));
return mem;
} else {
/* error already captured */
if (!expand(s, allocated(s) + z1)) [[unlikely]] {
/* (error state already captured) */
return nullptr;
}
if (store_header_flag) {
/* capturing header */
*(uint64_t *)s.free_ = header;
if (retain_header_flag) {
/* and rembering for subsequent
* sub_alloc()
*/
s.last_header_ = (uint64_t *)s.free_;
}
}
byte * mem = s.free_ + hz;
s.free_ += z1;
if (store_guard) {
/* write guard bytes for overrun detection */
::memset(s.free_,
s.config_.guard_byte_,
s.config_.guard_z_);
s.free_ += s.config_.guard_z_;
}
log && log(xtag("self", s.config_.name_),
xtag("hz", hz),
xtag("z0", req_z),
xtag("+pad", dz),
xtag("z1", z1),
xtag("size", size(s)),
xtag("avail", available(s)));
return mem;
}
void

View file

@ -212,6 +212,59 @@ namespace xo {
REQUIRE(a1o.committed() <= a1o.reserved());
}
TEST_CASE("allocator-alloc-3", "[alloc2][Allocator]")
{
using header_type = AAllocator::header_type;
/* typed allocator a1o, with object header + guard bytes */
ArenaConfig cfg { .name_ = "testarena",
.size_ = 64*1024,
.guard_z_ = 8,
.guard_byte_ = 0xfd,
.store_header_flag_ = true,
/* up to 4GB */
.header_size_mask_ = 0xffffffff,
.debug_flag_ = false,
};
DArena arena = DArena::map(cfg);
obj<AAllocator, DArena> a1o{&arena};
REQUIRE(a1o.reserved() >= cfg.size_);
REQUIRE(a1o.committed() == 0);
REQUIRE(a1o.available() == 0);
REQUIRE(a1o.allocated() == 0);
size_t z0 = 1;
byte * m0 = a1o.alloc(1);
REQUIRE(m0);
//
// > <
// < guard><header> < pad >< guard>
// ++++++++0000zzzzXppppppp++++++++
// ^ ^ ^ ^
// guard0 header m0 guard1
//
byte * guard0 = m0 - sizeof(header_type) - cfg.guard_z_;
header_type* header = (header_type*)(m0 - sizeof(header_type));
size_t pad = padding::with_padding(z0) - z0;
byte * guard1 = m0 + z0 + pad;
REQUIRE(a1o.contains(guard0));
REQUIRE(a1o.contains(header));
REQUIRE(((*header) & cfg.header_size_mask_) == padding::with_padding(z0));
REQUIRE(a1o.last_error().error_ == error::none);
REQUIRE(a1o.last_error().error_seq_ == 0);
REQUIRE(a1o.allocated() == cfg.guard_z_ + sizeof(header_type) + z0 + pad + cfg.guard_z_);
REQUIRE(a1o.allocated() <= a1o.committed());
REQUIRE(a1o.allocated() + a1o.available() == a1o.committed());
REQUIRE(a1o.committed() <= a1o.reserved());
}
TEST_CASE("allocator-fail-1", "[alloc2][AAllocator]")
{
/* typed allocator a1o */