aboutsummaryrefslogtreecommitdiffstats
path: root/benches/bench_common.h
diff options
context:
space:
mode:
authorFabrice <fabrice@schaub-dev.xyz>2026-04-13 16:44:23 +0200
committerFabrice <fabrice@schaub-dev.xyz>2026-04-13 16:44:23 +0200
commit8889837da535b1164d0fc52e8afeb671ad993b32 (patch)
treef781d48197eee3819534fb9c9e6bccd2988561a0 /benches/bench_common.h
parentb7210745c9d3920db5e09f3ebbe0f9cb7051311c (diff)
fix file naming
Diffstat (limited to 'benches/bench_common.h')
-rw-r--r--benches/bench_common.h544
1 files changed, 544 insertions, 0 deletions
diff --git a/benches/bench_common.h b/benches/bench_common.h
new file mode 100644
index 0000000..1f3b4d3
--- /dev/null
+++ b/benches/bench_common.h
@@ -0,0 +1,544 @@
+#ifndef CHEESEMAP_BENCH_COMMON_HPP
+#define CHEESEMAP_BENCH_COMMON_HPP
+
+#include <benchmark/benchmark.h>
+
+#include <algorithm>
+#include <array>
+#include <cstdarg>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <random>
+#include <string_view>
+#include <type_traits>
+#include <unordered_map>
+#include <vector>
+
+#include "cheesemap.h"
+#include "tidwall/hashmap.h"
+#include "absl/container/flat_hash_map.h"
+
+
+namespace cmbench {
+
+inline constexpr std::int64_t kEntryCount = 1'000'000;
+inline constexpr std::string_view kProtocolVersion = "1";
+inline constexpr std::string_view kDatasetVersion = "1";
+inline constexpr std::string_view kHashQuality = "good";
+inline constexpr std::string_view kHashName = "xxh_avalanche";
+
+[[noreturn]] inline void panic_impl(const char* file, cm_u32 line,
+ const char* fmt, ...) {
+ std::fprintf(stderr, "panic at %s:%u: ", file, line);
+ va_list args;
+ va_start(args, fmt);
+ std::vfprintf(stderr, fmt, args);
+ va_end(args);
+ std::fputc('\n', stderr);
+ std::abort();
+}
+
+inline cm_u8* default_alloc(cm_usize size, cm_usize align, cm_u8* user) {
+ (void)user;
+ return static_cast<cm_u8*>(std::aligned_alloc(align, size));
+}
+
+inline void default_dealloc(cm_u8* ptr, cm_u8* user) {
+ (void)user;
+ std::free(ptr);
+}
+
+inline std::uint64_t rotl64(std::uint64_t x, unsigned bits) {
+ return (x << bits) | (x >> (64 - bits));
+}
+
+inline std::uint64_t xxh64_avalanche(std::uint64_t x) {
+ x ^= x >> 33;
+ x *= 0xc2b2ae3d27d4eb4fULL;
+ x ^= x >> 29;
+ x *= 0x165667b19e3779f9ULL;
+ x ^= x >> 32;
+ return x;
+}
+
+template <typename T>
+inline std::array<std::uint64_t, (sizeof(T) + 7) / 8> as_words(const T& value) {
+ static_assert(std::is_trivially_copyable_v<T>);
+ std::array<std::uint64_t, (sizeof(T) + 7) / 8> words{};
+ std::memcpy(words.data(), &value, sizeof(T));
+ return words;
+}
+
+template <typename T>
+inline std::uint64_t hash_xxh_avalanche(const T& value) {
+ auto words = as_words(value);
+ std::uint64_t acc = 0x165667b19e3779f9ULL ^ sizeof(T);
+ for (std::uint64_t word : words) {
+ acc += word * 0x9e3779b185ebca87ULL;
+ acc = rotl64(acc, 31);
+ acc *= 0xc2b2ae3d27d4eb4fULL;
+ }
+ return xxh64_avalanche(acc);
+}
+
+template <typename T>
+inline bool byte_equal(const T& lhs, const T& rhs) {
+ static_assert(std::is_trivially_copyable_v<T>);
+ return std::memcmp(&lhs, &rhs, sizeof(T)) == 0;
+}
+
+struct ScalarValue {
+ std::uint64_t value;
+};
+
+struct EntityId {
+ std::uint32_t index;
+ std::uint32_t generation;
+};
+
+struct ComponentPayload {
+ float position[3];
+ float velocity[3];
+ std::uint32_t archetype;
+ std::uint32_t chunk;
+};
+
+struct ComponentKey {
+ std::uint64_t archetype_mask;
+ std::uint32_t component_id;
+ std::uint32_t chunk;
+ std::uint32_t row;
+ std::uint32_t generation;
+};
+
+struct ComponentMeta {
+ std::uint64_t entity_mask;
+ std::uint32_t dense_index;
+ std::uint32_t sparse_index;
+ std::uint64_t version;
+};
+
+template <typename Key, typename Value>
+struct Workload {
+ std::vector<Key> keys;
+ std::vector<Key> miss_keys;
+ std::vector<Value> values;
+};
+
+template <typename Key>
+inline void shuffle_keys(std::vector<Key>& keys, std::mt19937_64& rng) {
+ std::shuffle(keys.begin(), keys.end(), rng);
+}
+
+inline Workload<std::uint64_t, ScalarValue> make_scalar_workload(
+ std::size_t count) {
+ Workload<std::uint64_t, ScalarValue> workload{
+ .keys = std::vector<std::uint64_t>(count),
+ .miss_keys = std::vector<std::uint64_t>(count),
+ .values = std::vector<ScalarValue>(count),
+ };
+
+ std::mt19937_64 rng(0xc0ffee5eedULL);
+ for (std::size_t i = 0; i < count; ++i) {
+ workload.keys[i] = xxh64_avalanche(i + 1);
+ workload.miss_keys[i] = xxh64_avalanche(i + 1 + count);
+ workload.values[i] =
+ ScalarValue{xxh64_avalanche(i ^ 0x123456789abcdef0ULL)};
+ }
+ shuffle_keys(workload.keys, rng);
+ shuffle_keys(workload.miss_keys, rng);
+ return workload;
+}
+
+inline Workload<EntityId, ComponentPayload> make_entity_workload(
+ std::size_t count) {
+ Workload<EntityId, ComponentPayload> workload{
+ .keys = std::vector<EntityId>(count),
+ .miss_keys = std::vector<EntityId>(count),
+ .values = std::vector<ComponentPayload>(count),
+ };
+
+ std::mt19937_64 rng(0xEC500123ULL);
+ for (std::size_t i = 0; i < count; ++i) {
+ workload.keys[i] = EntityId{static_cast<std::uint32_t>(i),
+ static_cast<std::uint32_t>(i >> 12)};
+ workload.miss_keys[i] = EntityId{static_cast<std::uint32_t>(i),
+ static_cast<std::uint32_t>((i >> 12) + 1)};
+
+ float base = static_cast<float>(i & 0xffff);
+ workload.values[i] = ComponentPayload{
+ .position = {base + 0.1f, base + 0.2f, base + 0.3f},
+ .velocity = {base + 1.1f, base + 1.2f, base + 1.3f},
+ .archetype = static_cast<std::uint32_t>(xxh64_avalanche(i) & 0xffff),
+ .chunk = static_cast<std::uint32_t>(i / 64),
+ };
+ }
+ shuffle_keys(workload.keys, rng);
+ shuffle_keys(workload.miss_keys, rng);
+ return workload;
+}
+
+inline Workload<ComponentKey, ComponentMeta> make_component_workload(
+ std::size_t count) {
+ Workload<ComponentKey, ComponentMeta> workload{
+ .keys = std::vector<ComponentKey>(count),
+ .miss_keys = std::vector<ComponentKey>(count),
+ .values = std::vector<ComponentMeta>(count),
+ };
+
+ std::mt19937_64 rng(0xA11CECC5ULL);
+ for (std::size_t i = 0; i < count; ++i) {
+ std::uint64_t archetype = xxh64_avalanche(i * 0x9e3779b97f4a7c15ULL);
+ workload.keys[i] = ComponentKey{
+ .archetype_mask = archetype,
+ .component_id = static_cast<std::uint32_t>((i * 17) & 0xffff),
+ .chunk = static_cast<std::uint32_t>(i / 64),
+ .row = static_cast<std::uint32_t>(i % 64),
+ .generation = static_cast<std::uint32_t>(i >> 10),
+ };
+ workload.miss_keys[i] = ComponentKey{
+ .archetype_mask = archetype ^ 0xfeedfacecafebeefULL,
+ .component_id = static_cast<std::uint32_t>((i * 17) & 0xffff),
+ .chunk = static_cast<std::uint32_t>(i / 64),
+ .row = static_cast<std::uint32_t>(i % 64),
+ .generation = static_cast<std::uint32_t>(i >> 10),
+ };
+ workload.values[i] = ComponentMeta{
+ .entity_mask = xxh64_avalanche(i + 7),
+ .dense_index = static_cast<std::uint32_t>(i),
+ .sparse_index =
+ static_cast<std::uint32_t>(xxh64_avalanche(i) & 0xffffffffU),
+ .version = xxh64_avalanche(i ^ 0xabcdef01ULL),
+ };
+ }
+ shuffle_keys(workload.keys, rng);
+ shuffle_keys(workload.miss_keys, rng);
+ return workload;
+}
+
+inline const Workload<std::uint64_t, ScalarValue>& scalar_workload() {
+ static const auto workload = make_scalar_workload(kEntryCount);
+ return workload;
+}
+
+inline const Workload<EntityId, ComponentPayload>& entity_workload() {
+ static const auto workload = make_entity_workload(kEntryCount);
+ return workload;
+}
+
+inline const Workload<ComponentKey, ComponentMeta>& component_workload() {
+ static const auto workload = make_component_workload(kEntryCount);
+ return workload;
+}
+
+template <typename T>
+inline cm_hash_t cheesemap_hash_adapter(const cm_u8* key, cm_u8* user) {
+ (void)user;
+ return hash_xxh_avalanche(*reinterpret_cast<const T*>(key));
+}
+
+template <typename T>
+inline bool cheesemap_equal_adapter(const cm_u8* lhs, const cm_u8* rhs,
+ cm_u8* user) {
+ (void)user;
+ return byte_equal(*reinterpret_cast<const T*>(lhs),
+ *reinterpret_cast<const T*>(rhs));
+}
+
+struct Meta {
+ std::string_view implementation;
+ std::string_view language;
+ std::string_view dataset;
+ std::string_view workload_category;
+};
+
+inline void set_common_metadata(benchmark::State& state, const Meta& meta) {
+ std::string label;
+ label.reserve(128);
+ label.append("implementation=");
+ label.append(meta.implementation);
+ label.append(",language=");
+ label.append(meta.language);
+ label.append(",dataset=");
+ label.append(meta.dataset);
+ label.append(",workload=");
+ label.append(meta.workload_category);
+ label.append(",hash_quality=");
+ label.append(kHashQuality);
+ label.append(",hash=");
+ label.append(kHashName);
+ label.append(",protocol=");
+ label.append(kProtocolVersion);
+ label.append(",dataset_version=");
+ label.append(kDatasetVersion);
+ state.SetLabel(label);
+ state.counters["entry_count"] =
+ benchmark::Counter(static_cast<double>(kEntryCount));
+}
+
+template <typename Key, typename Value>
+struct CheesemapAdapter {
+ cheesemap map;
+
+ CheesemapAdapter() {
+ cm_init(&map, sizeof(Key), alignof(Key), sizeof(Value), alignof(Value),
+ nullptr, cheesemap_hash_adapter<Key>, cheesemap_equal_adapter<Key>,
+ default_alloc, default_dealloc);
+ }
+
+ ~CheesemapAdapter() { cm_drop(&map); }
+
+ CheesemapAdapter(const CheesemapAdapter&) = delete;
+ CheesemapAdapter& operator=(const CheesemapAdapter&) = delete;
+
+ void reserve(std::size_t count) {
+ if (!cm_reserve(&map, count))
+ panic_impl(__FILE__, __LINE__, "reserve failed");
+ }
+
+ void insert(const Key& key, const Value& value) {
+ if (!cm_insert(&map, reinterpret_cast<const cm_u8*>(&key),
+ reinterpret_cast<const cm_u8*>(&value))) {
+ panic_impl(__FILE__, __LINE__, "insert failed");
+ }
+ }
+
+ Value* lookup_hit(const Key& key) {
+ cm_u8* value_ptr = nullptr;
+ if (!cm_lookup(&map, reinterpret_cast<const cm_u8*>(&key), &value_ptr))
+ panic_impl(__FILE__, __LINE__, "lookup hit failed");
+ return reinterpret_cast<Value*>(value_ptr);
+ }
+
+ void lookup_miss(const Key& key) {
+ cm_u8* value_ptr = nullptr;
+ if (cm_lookup(&map, reinterpret_cast<const cm_u8*>(&key), &value_ptr))
+ panic_impl(__FILE__, __LINE__, "lookup miss failed");
+ benchmark::DoNotOptimize(value_ptr);
+ }
+
+ void erase(const Key& key) {
+ if (!cm_remove(&map, reinterpret_cast<const cm_u8*>(&key), nullptr))
+ panic_impl(__FILE__, __LINE__, "erase failed");
+ }
+};
+
+template <typename Key>
+struct UnorderedHashAdapter {
+ std::size_t operator()(const Key& key) const noexcept {
+ return static_cast<std::size_t>(hash_xxh_avalanche(key));
+ }
+};
+
+template <typename Key>
+struct UnorderedEqualAdapter {
+ bool operator()(const Key& lhs, const Key& rhs) const noexcept {
+ return byte_equal(lhs, rhs);
+ }
+};
+
+template <typename Key, typename Value>
+struct UnorderedMapAdapter {
+ using Map = std::unordered_map<Key, Value, UnorderedHashAdapter<Key>,
+ UnorderedEqualAdapter<Key>>;
+
+ Map map;
+
+ void reserve(std::size_t count) { map.reserve(count); }
+
+ void insert(const Key& key, const Value& value) {
+ map.insert_or_assign(key, value);
+ }
+
+ Value* lookup_hit(const Key& key) {
+ auto it = map.find(key);
+ if (it == map.end()) panic_impl(__FILE__, __LINE__, "lookup hit failed");
+ return &it->second;
+ }
+
+ void lookup_miss(const Key& key) {
+ auto it = map.find(key);
+ if (it != map.end()) panic_impl(__FILE__, __LINE__, "lookup miss failed");
+ benchmark::DoNotOptimize(it);
+ }
+
+ void erase(const Key& key) {
+ if (map.erase(key) != 1) panic_impl(__FILE__, __LINE__, "erase failed");
+ }
+};
+
+template <typename Key, typename Value>
+struct TidwallEntry {
+ Key key;
+ Value value;
+};
+
+template <typename Key, typename Value>
+struct TidwallAdapter {
+ using Entry = TidwallEntry<Key, Value>;
+
+ hashmap* map = nullptr;
+
+ static std::uint64_t hash_entry(const void* item, std::uint64_t seed0,
+ std::uint64_t seed1) {
+ (void)seed0;
+ (void)seed1;
+ const auto* entry = static_cast<const Entry*>(item);
+ return hash_xxh_avalanche(entry->key);
+ }
+
+ static int compare_entry(const void* lhs, const void* rhs, void* udata) {
+ (void)udata;
+ const auto* left = static_cast<const Entry*>(lhs);
+ const auto* right = static_cast<const Entry*>(rhs);
+ return byte_equal(left->key, right->key) ? 0 : 1;
+ }
+
+ TidwallAdapter() {
+ map = hashmap_new(sizeof(Entry), static_cast<std::size_t>(kEntryCount), 0,
+ 0, hash_entry, compare_entry, nullptr, nullptr);
+ if (map == nullptr) panic_impl(__FILE__, __LINE__, "hashmap_new failed");
+ }
+
+ ~TidwallAdapter() {
+ if (map != nullptr) hashmap_free(map);
+ }
+
+ TidwallAdapter(const TidwallAdapter&) = delete;
+ TidwallAdapter& operator=(const TidwallAdapter&) = delete;
+
+ void reserve(std::size_t count) { benchmark::DoNotOptimize(count); }
+
+ void insert(const Key& key, const Value& value) {
+ const Entry entry{key, value};
+ if (hashmap_set(map, &entry) == nullptr && hashmap_oom(map))
+ panic_impl(__FILE__, __LINE__, "insert failed");
+ }
+
+ Value* lookup_hit(const Key& key) {
+ const Entry query{key, Value{}};
+ auto* found = static_cast<const Entry*>(hashmap_get(map, &query));
+ if (found == nullptr) panic_impl(__FILE__, __LINE__, "lookup hit failed");
+ return const_cast<Value*>(&found->value);
+ }
+
+ void lookup_miss(const Key& key) {
+ const Entry query{key, Value{}};
+ auto* found = static_cast<const Entry*>(hashmap_get(map, &query));
+ if (found != nullptr) panic_impl(__FILE__, __LINE__, "lookup miss failed");
+ benchmark::DoNotOptimize(found);
+ }
+
+ void erase(const Key& key) {
+ const Entry query{key, Value{}};
+ if (hashmap_delete(map, &query) == nullptr)
+ panic_impl(__FILE__, __LINE__, "erase failed");
+ }
+};
+
+template <typename Key, typename Value>
+struct AbseilAdapter {
+ using Map = absl::flat_hash_map<Key, Value, UnorderedHashAdapter<Key>,
+ UnorderedEqualAdapter<Key>>;
+
+ Map map;
+
+ void reserve(std::size_t count) { map.reserve(count); }
+
+ void insert(const Key& key, const Value& value) {
+ map.insert_or_assign(key, value);
+ }
+
+ Value* lookup_hit(const Key& key) {
+ auto it = map.find(key);
+ if (it == map.end()) panic_impl(__FILE__, __LINE__, "lookup hit failed");
+ return &it->second;
+ }
+
+ void lookup_miss(const Key& key) {
+ auto it = map.find(key);
+ if (it != map.end()) panic_impl(__FILE__, __LINE__, "lookup miss failed");
+ benchmark::DoNotOptimize(it);
+ }
+
+ void erase(const Key& key) {
+ if (map.erase(key) != 1) panic_impl(__FILE__, __LINE__, "erase failed");
+ }
+};
+
+template <typename Adapter, typename Key, typename Value>
+inline void fill_container(Adapter& adapter,
+ const Workload<Key, Value>& workload) {
+ adapter.reserve(workload.keys.size());
+ for (std::size_t i = 0; i < workload.keys.size(); ++i)
+ adapter.insert(workload.keys[i], workload.values[i]);
+}
+
+template <typename Adapter, typename Key, typename Value>
+inline void bench_insert(benchmark::State& state,
+ const Workload<Key, Value>& workload,
+ const Meta& meta) {
+ set_common_metadata(state, meta);
+ for (auto _ : state) {
+ Adapter adapter;
+ fill_container(adapter, workload);
+ benchmark::ClobberMemory();
+ }
+}
+
+template <typename Adapter, typename Key, typename Value>
+inline void bench_lookup_hit(benchmark::State& state,
+ const Workload<Key, Value>& workload,
+ const Meta& meta) {
+ set_common_metadata(state, meta);
+ for (auto _ : state) {
+ state.PauseTiming();
+ Adapter adapter;
+ fill_container(adapter, workload);
+ state.ResumeTiming();
+
+ for (const Key& key : workload.keys) {
+ Value* value_ptr = adapter.lookup_hit(key);
+ benchmark::DoNotOptimize(value_ptr);
+ }
+ benchmark::ClobberMemory();
+ }
+}
+
+template <typename Adapter, typename Key, typename Value>
+inline void bench_lookup_miss(benchmark::State& state,
+ const Workload<Key, Value>& workload,
+ const Meta& meta) {
+ set_common_metadata(state, meta);
+ for (auto _ : state) {
+ state.PauseTiming();
+ Adapter adapter;
+ fill_container(adapter, workload);
+ state.ResumeTiming();
+
+ for (const Key& key : workload.miss_keys) adapter.lookup_miss(key);
+ benchmark::ClobberMemory();
+ }
+}
+
+template <typename Adapter, typename Key, typename Value>
+inline void bench_erase(benchmark::State& state,
+ const Workload<Key, Value>& workload,
+ const Meta& meta) {
+ set_common_metadata(state, meta);
+ for (auto _ : state) {
+ state.PauseTiming();
+ Adapter adapter;
+ fill_container(adapter, workload);
+ state.ResumeTiming();
+
+ for (const Key& key : workload.keys) adapter.erase(key);
+ benchmark::ClobberMemory();
+ }
+}
+
+} // namespace cmbench
+
+#endif