diff --git a/common/flatmap.h b/common/flatmap.h new file mode 100644 index 00000000..28c70e5a --- /dev/null +++ b/common/flatmap.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include + +#include "common/crc32.h" +#include "common/memory.h" +#include "common/memory_address.h" + +struct flatmap { + void *keys; + void *values; + uint8_t *present; + size_t capacity; +}; + +static inline void +flat_map_free( + struct flatmap *map, + struct memory_context *mctx, + size_t key_size, + size_t value_size +) { + if (map->keys != NULL) { + memory_bfree( + mctx, ADDR_OF(&map->keys), key_size * map->capacity + ); + } + if (map->values != NULL) { + memory_bfree( + mctx, ADDR_OF(&map->values), value_size * map->capacity + ); + } + if (map->present != NULL) { + memory_bfree(mctx, ADDR_OF(&map->present), map->capacity); + } + memset(map, 0, sizeof(*map)); +} + +static inline int +flat_map_build( + struct flatmap *map, + struct memory_context *mctx, + size_t capacity, + size_t count, + const void *keys, + size_t key_size, + const void *values, + size_t value_size +) { + if (capacity <= count) { + return -1; + } + + size_t keys_memory = key_size * capacity; + size_t values_memory = value_size * capacity; + memset(map, 0, sizeof(*map)); + + map->capacity = capacity; + + map->keys = memory_balloc(mctx, keys_memory); + SET_OFFSET_OF(&map->keys, map->keys); + if (map->keys == NULL) { + return -1; + } + + map->values = memory_balloc(mctx, values_memory); + SET_OFFSET_OF(&map->values, map->values); + if (map->values == NULL) { + flat_map_free(map, mctx, key_size, value_size); + return -1; + } + + uint8_t *present = memory_balloc(mctx, capacity); + if (present == NULL) { + flat_map_free(map, mctx, key_size, value_size); + return -1; + } + memset(present, 0, capacity); + SET_OFFSET_OF(&map->present, present); + + for (size_t i = 0; i < count; ++i) { + size_t j = crc32(keys + i * key_size, key_size, 0) % capacity; + while (present[j]) { + ++j; + if (j == capacity) { + j = 0; + } + } + present[j] = 1; + memcpy(ADDR_OF(&map->keys) + j * key_size, + keys + i * key_size, + key_size); + memcpy(ADDR_OF(&map->values) + j * value_size, + values + i * value_size, + value_size); + } + + return 0; +} + +static inline void * +flat_map_lookup( + struct flatmap *map, const void *key, size_t key_size, size_t value_size +) { + const void *keys = ADDR_OF(&map->keys); + void *values = ADDR_OF(&map->values); + uint8_t *present = ADDR_OF(&map->present); + size_t j = crc32(key, key_size, 0) % map->capacity; + while (present[j]) { + if (memcmp(keys + j * key_size, key, key_size) == 0) { + return values + j * value_size; + } + ++j; + if (j == map->capacity) { + j = 0; + } + } + return NULL; +} \ No newline at end of file diff --git a/tests/common/flatmap_test.c b/tests/common/flatmap_test.c new file mode 100644 index 00000000..0c1a1112 --- /dev/null +++ b/tests/common/flatmap_test.c @@ -0,0 +1,167 @@ +#include "common/flatmap.h" +#include "common/memory.h" +#include "common/memory_block.h" +#include "common/test_assert.h" +#include "lib/logging/log.h" + +#include +#include +#include + +static int +setup_allocator(struct block_allocator *ba, void **raw_mem, size_t size) { + TEST_ASSERT( + block_allocator_init(ba) == 0, "block_allocator_init failed" + ); + + *raw_mem = malloc(size); + TEST_ASSERT(*raw_mem != NULL, "failed to allocate raw memory"); + + block_allocator_put_arena(ba, *raw_mem, size); + return TEST_SUCCESS; +} + +static int +test_zero_count(void) { + LOG(INFO, "Test zero count..."); + + struct block_allocator ba; + void *raw_mem = NULL; + const size_t arena_size = 1 << 20; + TEST_ASSERT( + setup_allocator(&ba, &raw_mem, arena_size) == TEST_SUCCESS, + "setup allocator" + ); + + struct memory_context mctx; + TEST_ASSERT( + memory_context_init(&mctx, "flatmap", &ba) == 0, + "init memory context" + ); + + struct flatmap map; + int res = flat_map_build( + &map, + &mctx, + 8, + 0, + NULL, + sizeof(uint32_t), + NULL, + sizeof(uint32_t) + ); + TEST_ASSERT(res == 0, "build flat map"); + + uint32_t probes[] = {0, 1, 42, 0xDEADBEEFu}; + for (size_t idx = 0; idx < sizeof(probes) / sizeof(probes[0]); ++idx) { + void *found = flat_map_lookup( + &map, &probes[idx], sizeof(uint32_t), sizeof(uint32_t) + ); + TEST_ASSERT_NULL(found, "lookup on empty map must return NULL"); + } + + flat_map_free(&map, &mctx, sizeof(uint32_t), sizeof(uint32_t)); + TEST_ASSERT_EQUAL( + mctx.bfree_count, + mctx.balloc_count, + "memory context: free count != alloc count" + ); + TEST_ASSERT_EQUAL( + mctx.bfree_size, + mctx.balloc_size, + "memory context: free size != alloc size" + ); + free(raw_mem); + return TEST_SUCCESS; +} + +static int +test_basic_lookup(void) { + LOG(INFO, "Test basic lookup..."); + + struct block_allocator ba; + void *raw_mem = NULL; + const size_t arena_size = 1 << 20; + TEST_ASSERT( + setup_allocator(&ba, &raw_mem, arena_size) == TEST_SUCCESS, + "setup allocator" + ); + + struct memory_context mctx; + TEST_ASSERT( + memory_context_init(&mctx, "flatmap", &ba) == 0, + "init memory context" + ); + + const uint32_t keys[] = {1, 2, 3, 100, 200}; + const uint32_t values[] = {10, 20, 30, 1000, 2000}; + const size_t count = sizeof(keys) / sizeof(keys[0]); + + struct flatmap map; + int res = flat_map_build( + &map, + &mctx, + count * 2, + count, + keys, + sizeof(uint32_t), + values, + sizeof(uint32_t) + ); + TEST_ASSERT(res == 0, "build flat map"); + + for (size_t idx = 0; idx < count; ++idx) { + uint32_t key = keys[idx]; + uint32_t *value = (uint32_t *)flat_map_lookup( + &map, &key, sizeof(uint32_t), sizeof(uint32_t) + ); + TEST_ASSERT_NOT_NULL( + value, "lookup of present key %u returned NULL", key + ); + TEST_ASSERT_EQUAL( + *value, values[idx], "value mismatch for key %u", key + ); + } + + const uint32_t missing[] = {0, 4, 99, 500, 0xFFFFFFFFu}; + for (size_t idx = 0; idx < sizeof(missing) / sizeof(missing[0]); + ++idx) { + void *value = flat_map_lookup( + &map, &missing[idx], sizeof(uint32_t), sizeof(uint32_t) + ); + TEST_ASSERT_NULL( + value, + "lookup of missing key %u returned non-NULL", + missing[idx] + ); + } + + flat_map_free(&map, &mctx, sizeof(uint32_t), sizeof(uint32_t)); + TEST_ASSERT_EQUAL( + mctx.bfree_count, + mctx.balloc_count, + "memory context: free count != alloc count" + ); + TEST_ASSERT_EQUAL( + mctx.bfree_size, + mctx.balloc_size, + "memory context: free size != alloc size" + ); + free(raw_mem); + return TEST_SUCCESS; +} + +int +main(void) { + log_enable_name("info"); + + if (test_zero_count() != TEST_SUCCESS) { + return -1; + } + if (test_basic_lookup() != TEST_SUCCESS) { + return -1; + } + + LOG(INFO, "All flatmap tests passed"); + return 0; +} \ No newline at end of file diff --git a/tests/common/meson.build b/tests/common/meson.build index e6d00028..002abffa 100644 --- a/tests/common/meson.build +++ b/tests/common/meson.build @@ -124,3 +124,14 @@ data_pipe_test = executable( include_directories: yanet_rootdir, ) test('data_pipe', data_pipe_test, suite: ['common']) + +flatmap_test = executable( + 'flatmap_test', + ['flatmap_test.c'], + c_args: yanet_test_c_args, + link_args: yanet_link_args, + dependencies: [lib_common_dep, lib_logging_dep], + include_directories: yanet_rootdir, +) +test('flatmap', flatmap_test, suite: ['common']) +