@@ -2715,39 +2715,43 @@ static constexpr std::string_view malicious_json = R"({
27152715
27162716static 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 */
27592763inline 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
29052913BENCHMARK (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 >" );
29072915BENCHMARK (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
29702978template <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
29993007template <typename json_type_>
@@ -3017,14 +3025,15 @@ bool contains_xss_nlohmann(json_type_ const &j) noexcept {
30173025}
30183026
30193027using 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
30223030enum class exception_handling_t { throw_k, noexcept_k };
30233031
30243032template <typename json_type_, exception_handling_t exception_handling_>
30253033static 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
30613074BENCHMARK (json_nlohmann<default_json, exception_handling_t ::throw_k>)
30623075 ->MinTime(10 )
30633076 ->Name(" json_nlohmann<std::allocator, throw>" );
30643077BENCHMARK (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>" );
30673080BENCHMARK (json_nlohmann<default_json, exception_handling_t ::noexcept_k>)
30683081 ->MinTime(10 )
30693082 ->Name(" json_nlohmann<std::allocator, noexcept>" );
30703083BENCHMARK (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>" );
30733086BENCHMARK (json_nlohmann<default_json, exception_handling_t ::throw_k>)
30743087 ->MinTime(10 )
30753088 ->Name(" json_nlohmann<std::allocator, throw>" )
30763089 ->Threads(physical_cores());
30773090BENCHMARK (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());
30813094BENCHMARK (json_nlohmann<default_json, exception_handling_t ::noexcept_k>)
30823095 ->MinTime(10 )
30833096 ->Name(" json_nlohmann<std::allocator, noexcept>" )
30843097 ->Threads(physical_cores());
30853098BENCHMARK (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