Skip to content

Commit 83ba346

Browse files
committed
Improve: Log mean allocation size
1 parent ce7f21f commit 83ba346

File tree

1 file changed

+47
-34
lines changed

1 file changed

+47
-34
lines changed

less_slow.cpp

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2715,39 +2715,43 @@ static constexpr std::string_view malicious_json = R"({
27152715

27162716
static constexpr std::string_view packets_json[3] = {valid_json, invalid_json, malicious_json};
27172717

2718-
struct fixed_buffer_arena_t {
2719-
static constexpr std::size_t capacity = 4096;
2720-
alignas(64) std::byte buffer[capacity];
2718+
struct limited_arena_t {
2719+
static constexpr std::size_t capacity_k = 4096;
2720+
alignas(64) std::byte buffer[capacity_k];
27212721

27222722
/// The offset (in bytes) of the next free location
27232723
std::size_t total_allocated = 0;
27242724
/// The total bytes "freed" so far
27252725
std::size_t total_reclaimed = 0;
2726+
/// The total number of unique allocations before a reset
2727+
std::size_t unique_allocations = 0;
27262728
};
27272729

27282730
/**
27292731
* @brief Allocates a new chunk of `size` bytes from the arena.
27302732
* @return The new pointer or `nullptr` if OOM.
27312733
*/
2732-
inline std::byte *allocate_from_arena(fixed_buffer_arena_t &arena, std::size_t size) noexcept {
2733-
if (arena.total_allocated + size > fixed_buffer_arena_t::capacity) return nullptr; // Not enough space
2734+
inline std::byte *allocate_from_arena(limited_arena_t &arena, std::size_t size) noexcept {
2735+
if (arena.total_allocated + size > limited_arena_t::capacity_k) return nullptr; // Not enough space
27342736
std::byte *ptr = arena.buffer + arena.total_allocated;
27352737
arena.total_allocated += size;
2738+
arena.unique_allocations++;
27362739
return ptr;
27372740
}
27382741

27392742
/**
27402743
* @brief Deallocates a chunk of memory previously allocated from the arena.
27412744
* This implementation does not "reuse" partial free space unless everything is freed.
27422745
*/
2743-
inline void deallocate_from_arena(fixed_buffer_arena_t &arena, std::byte *ptr, std::size_t size) noexcept {
2746+
inline void deallocate_from_arena(limited_arena_t &arena, std::byte *ptr, std::size_t size) noexcept {
27442747
// Check if ptr is within the arena
27452748
std::byte *start = arena.buffer;
2746-
std::byte *end = arena.buffer + fixed_buffer_arena_t::capacity;
2749+
std::byte *end = arena.buffer + limited_arena_t::capacity_k;
27472750
if (ptr < start || ptr >= end) return; // Invalid pointer => no-op
27482751
arena.total_reclaimed += size;
27492752
// Reset completely if fully reclaimed
2750-
if (arena.total_allocated == arena.total_reclaimed) arena.total_allocated = 0, arena.total_reclaimed = 0;
2753+
if (arena.total_allocated == arena.total_reclaimed)
2754+
arena.total_allocated = 0, arena.total_reclaimed = 0, arena.unique_allocations = 0;
27512755
}
27522756

27532757
/**
@@ -2757,7 +2761,7 @@ inline void deallocate_from_arena(fixed_buffer_arena_t &arena, std::byte *ptr, s
27572761
* @return The new pointer or `nullptr` if OOM.
27582762
*/
27592763
inline std::byte *reallocate_from_arena( //
2760-
fixed_buffer_arena_t &arena, std::byte *ptr, std::size_t old_size, std::size_t new_size) noexcept {
2764+
limited_arena_t &arena, std::byte *ptr, std::size_t old_size, std::size_t new_size) noexcept {
27612765
if (!ptr) return allocate_from_arena(arena, new_size); // A fresh allocation
27622766

27632767
// This is effectively a `free` operation
@@ -2774,7 +2778,7 @@ inline std::byte *reallocate_from_arena( //
27742778
// Expand in-place if there's enough room
27752779
std::size_t offset = static_cast<std::size_t>(ptr - arena.buffer);
27762780
std::size_t required_space = offset + new_size;
2777-
if (required_space <= fixed_buffer_arena_t::capacity) {
2781+
if (required_space <= limited_arena_t::capacity_k) {
27782782
// We can grow (or shrink) in place
27792783
arena.total_allocated = required_space;
27802784
return ptr;
@@ -2844,7 +2848,7 @@ static void json_yyjson(bm::State &state) {
28442848
// char yyjson_buffer[4096];
28452849
// yyjson_alc_pool_init(&alc, yyjson_buffer, sizeof(yyjson_buffer));
28462850
//
2847-
using arena_t = fixed_buffer_arena_t;
2851+
using arena_t = limited_arena_t;
28482852
arena_t arena;
28492853
yyjson_alc alc;
28502854
alc.ctx = &arena;
@@ -2855,7 +2859,7 @@ static void json_yyjson(bm::State &state) {
28552859
using alc_size_t = std::uint16_t;
28562860
alc.malloc = +[](void *ctx, size_t size_native) noexcept -> void * {
28572861
alc_size_t size = static_cast<alc_size_t>(size_native);
2858-
std::byte *result = allocate_from_arena(*static_cast<fixed_buffer_arena_t *>(ctx), size + sizeof(alc_size_t));
2862+
std::byte *result = allocate_from_arena(*static_cast<limited_arena_t *>(ctx), size + sizeof(alc_size_t));
28592863
if (!result) return nullptr;
28602864
std::memcpy(result, &size, sizeof(alc_size_t));
28612865
return (void *)(result + sizeof(alc_size_t));
@@ -2882,6 +2886,7 @@ static void json_yyjson(bm::State &state) {
28822886
// Repeat the checks many times
28832887
std::size_t bytes_processed = 0;
28842888
std::size_t peak_memory_usage = 0;
2889+
std::size_t peak_memory_calls = 0;
28852890
std::size_t iteration = 0;
28862891
for (auto _ : state) {
28872892

@@ -2896,16 +2901,19 @@ static void json_yyjson(bm::State &state) {
28962901
YYJSON_READ_NOFLAG, use_arena ? &alc : NULL, &error);
28972902
if (!error.code) bm::DoNotOptimize(contains_xss_in_yyjson(yyjson_doc_get_root(doc)));
28982903
peak_memory_usage = std::max(peak_memory_usage, arena.total_allocated);
2904+
peak_memory_calls = std::max(peak_memory_calls, arena.unique_allocations);
28992905
yyjson_doc_free(doc);
29002906
}
29012907
state.SetBytesProcessed(bytes_processed);
29022908
state.counters["peak_memory_usage"] = bm::Counter(peak_memory_usage, bm::Counter::kAvgThreads);
2909+
state.counters["mean_allocation_size"] =
2910+
bm::Counter(peak_memory_usage * 1.0 / peak_memory_calls, bm::Counter::kAvgThreads);
29032911
}
29042912

29052913
BENCHMARK(json_yyjson<false>)->MinTime(10)->Name("json_yyjson<malloc>");
2906-
BENCHMARK(json_yyjson<true>)->MinTime(10)->Name("json_yyjson<fixed_buffer>");
2914+
BENCHMARK(json_yyjson<true>)->MinTime(10)->Name("json_yyjson<limited_arena>");
29072915
BENCHMARK(json_yyjson<false>)->MinTime(10)->Name("json_yyjson<malloc>")->Threads(physical_cores());
2908-
BENCHMARK(json_yyjson<true>)->MinTime(10)->Name("json_yyjson<fixed_buffer>")->Threads(physical_cores());
2916+
BENCHMARK(json_yyjson<true>)->MinTime(10)->Name("json_yyjson<limited_arena>")->Threads(physical_cores());
29092917

29102918
/**
29112919
* The `nlohmann::json` library is designed to be simple and easy to use, but it's
@@ -2947,7 +2955,7 @@ using json_with_alloc = nlohmann::basic_json< //
29472955

29482956
/**
29492957
* The `allocate_from_arena` and `deallocate_from_arena` are fairly elegant and simple.
2950-
* But we have no way of supplying our `fixed_buffer_arena_t` instance to the `nlohmann::json`
2958+
* But we have no way of supplying our `limited_arena_t` instance to the `nlohmann::json`
29512959
* library and it has no mechanism internally to propagate the allocator state to the nested
29522960
* containers:
29532961
*
@@ -2965,35 +2973,35 @@ using json_with_alloc = nlohmann::basic_json< //
29652973
* which is an immediate @b code-smell, while with `yyjson` we can pass a context object down!
29662974
*/
29672975

2968-
thread_local fixed_buffer_arena_t local_arena;
2976+
thread_local limited_arena_t limited_arena;
29692977

29702978
template <typename value_type_>
2971-
struct fixed_buffer_allocator {
2979+
struct limited_allocator {
29722980
using value_type = value_type_;
29732981

2974-
fixed_buffer_allocator() noexcept = default;
2982+
limited_allocator() noexcept = default;
29752983

29762984
template <typename other_type_>
2977-
fixed_buffer_allocator(fixed_buffer_allocator<other_type_> const &) noexcept {}
2985+
limited_allocator(limited_allocator<other_type_> const &) noexcept {}
29782986

29792987
value_type *allocate(std::size_t n) noexcept(false) {
2980-
if (auto ptr = allocate_from_arena(local_arena, n * sizeof(value_type)); ptr)
2988+
if (auto ptr = allocate_from_arena(limited_arena, n * sizeof(value_type)); ptr)
29812989
return reinterpret_cast<value_type *>(ptr);
29822990
else
29832991
throw std::bad_alloc();
29842992
}
29852993

29862994
void deallocate(value_type *ptr, std::size_t n) noexcept {
2987-
deallocate_from_arena(local_arena, reinterpret_cast<std::byte *>(ptr), n * sizeof(value_type));
2995+
deallocate_from_arena(limited_arena, reinterpret_cast<std::byte *>(ptr), n * sizeof(value_type));
29882996
}
29892997

29902998
// Rebind mechanism and comparators are for compatibility with STL containers
29912999
template <typename other_type_>
29923000
struct rebind {
2993-
using other = fixed_buffer_allocator<other_type_>;
3001+
using other = limited_allocator<other_type_>;
29943002
};
2995-
bool operator==(fixed_buffer_allocator const &) const noexcept { return true; }
2996-
bool operator!=(fixed_buffer_allocator const &) const noexcept { return false; }
3003+
bool operator==(limited_allocator const &) const noexcept { return true; }
3004+
bool operator!=(limited_allocator const &) const noexcept { return false; }
29973005
};
29983006

29993007
template <typename json_type_>
@@ -3017,14 +3025,15 @@ bool contains_xss_nlohmann(json_type_ const &j) noexcept {
30173025
}
30183026

30193027
using default_json = json_with_alloc<std::allocator>;
3020-
using fixed_buffer_json = json_with_alloc<fixed_buffer_allocator>;
3028+
using fixed_buffer_json = json_with_alloc<limited_allocator>;
30213029

30223030
enum class exception_handling_t { throw_k, noexcept_k };
30233031

30243032
template <typename json_type_, exception_handling_t exception_handling_>
30253033
static void json_nlohmann(bm::State &state) {
30263034
std::size_t bytes_processed = 0;
30273035
std::size_t peak_memory_usage = 0;
3036+
std::size_t peak_memory_calls = 0;
30283037
std::size_t iteration = 0;
30293038
for (auto _ : state) {
30303039

@@ -3051,40 +3060,44 @@ static void json_nlohmann(bm::State &state) {
30513060
json = json_type_::parse(packet_json, nullptr, false);
30523061
if (!json.is_discarded()) bm::DoNotOptimize(contains_xss_nlohmann(json));
30533062
}
3054-
if constexpr (!std::is_same_v<json_type_, default_json>)
3055-
peak_memory_usage = std::max(peak_memory_usage, local_arena.total_allocated);
3063+
if constexpr (!std::is_same_v<json_type_, default_json>) {
3064+
peak_memory_usage = std::max(peak_memory_usage, limited_arena.total_allocated);
3065+
peak_memory_calls = std::max(peak_memory_calls, limited_arena.unique_allocations);
3066+
}
30563067
}
30573068
state.SetBytesProcessed(bytes_processed);
30583069
state.counters["peak_memory_usage"] = bm::Counter(peak_memory_usage, bm::Counter::kAvgThreads);
3070+
state.counters["mean_allocation_size"] =
3071+
bm::Counter(peak_memory_usage * 1.0 / peak_memory_calls, bm::Counter::kAvgThreads);
30593072
}
30603073

30613074
BENCHMARK(json_nlohmann<default_json, exception_handling_t::throw_k>)
30623075
->MinTime(10)
30633076
->Name("json_nlohmann<std::allocator, throw>");
30643077
BENCHMARK(json_nlohmann<fixed_buffer_json, exception_handling_t::throw_k>)
30653078
->MinTime(10)
3066-
->Name("json_nlohmann<fixed_buffer, throw>");
3079+
->Name("json_nlohmann<limited_arena, throw>");
30673080
BENCHMARK(json_nlohmann<default_json, exception_handling_t::noexcept_k>)
30683081
->MinTime(10)
30693082
->Name("json_nlohmann<std::allocator, noexcept>");
30703083
BENCHMARK(json_nlohmann<fixed_buffer_json, exception_handling_t::noexcept_k>)
30713084
->MinTime(10)
3072-
->Name("json_nlohmann<fixed_buffer, noexcept>");
3085+
->Name("json_nlohmann<limited_arena, noexcept>");
30733086
BENCHMARK(json_nlohmann<default_json, exception_handling_t::throw_k>)
30743087
->MinTime(10)
30753088
->Name("json_nlohmann<std::allocator, throw>")
30763089
->Threads(physical_cores());
30773090
BENCHMARK(json_nlohmann<fixed_buffer_json, exception_handling_t::throw_k>)
30783091
->MinTime(10)
3079-
->Name("json_nlohmann<fixed_buffer, throw>")
3092+
->Name("json_nlohmann<limited_arena, throw>")
30803093
->Threads(physical_cores());
30813094
BENCHMARK(json_nlohmann<default_json, exception_handling_t::noexcept_k>)
30823095
->MinTime(10)
30833096
->Name("json_nlohmann<std::allocator, noexcept>")
30843097
->Threads(physical_cores());
30853098
BENCHMARK(json_nlohmann<fixed_buffer_json, exception_handling_t::noexcept_k>)
30863099
->MinTime(10)
3087-
->Name("json_nlohmann<fixed_buffer, noexcept>")
3100+
->Name("json_nlohmann<limited_arena, noexcept>")
30883101
->Threads(physical_cores());
30893102

30903103
/**
@@ -3093,11 +3106,11 @@ BENCHMARK(json_nlohmann<fixed_buffer_json, exception_handling_t::noexcept_k>)
30933106
* cores, are as follows:
30943107
*
30953108
* - `json_yyjson<malloc>`: @b 359 ns @b 369 ns
3096-
* - `json_yyjson<fixed_buffer>`: @b 326 ns @b 326 ns
3109+
* - `json_yyjson<limited_arena>`: @b 326 ns @b 326 ns
30973110
* - `json_nlohmann<std::allocator, throw>`: @b 6'440 ns @b 11'821 ns
3098-
* - `json_nlohmann<fixed_buffer, throw>`: @b 6'041 ns @b 11'601 ns
3111+
* - `json_nlohmann<limited_arena, throw>`: @b 6'041 ns @b 11'601 ns
30993112
* - `json_nlohmann<std::allocator, noexcept>`: @b 4'741 ns @b 11'512 ns
3100-
* - `json_nlohmann<fixed_buffer, noexcept>`: @b 4'316 ns @b 12'209 ns
3113+
* - `json_nlohmann<limited_arena, noexcept>`: @b 4'316 ns @b 12'209 ns
31013114
*
31023115
* The reason, why `yyjson` numbers are less affected by the allocator change,
31033116
* is because it doesn't need many dynamic allocations. It manages a linked list

0 commit comments

Comments
 (0)