From 6cdb667fa4651eec3aa0570df7539ccf77e9ef35 Mon Sep 17 00:00:00 2001 From: Ludovic 'Archivist' Lagouardette Date: Fri, 20 Nov 2020 16:40:20 +0100 Subject: [PATCH] push lock free hash table --- .gitignore | 1 + .vscode/launch.json | 16 ++++ .vscode/settings.json | 6 ++ include/lfhmap.hpp | 198 ++++++++++++++++++++++++++++++++++++++++++ tests.sh | 13 +++ tests/test01.cpp | 21 +++++ tests/test02.cpp | 22 +++++ tests/test03.cpp | 30 +++++++ tests/test04.cpp | 42 +++++++++ 9 files changed, 349 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 include/lfhmap.hpp create mode 100755 tests.sh create mode 100644 tests/test01.cpp create mode 100644 tests/test02.cpp create mode 100644 tests/test03.cpp create mode 100644 tests/test04.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cba7efc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +a.out diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..19334d5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "gdb", + "request": "launch", + "target": "./a.out", + "cwd": "${workspaceRoot}", + "valuesFormatting": "parseText" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..74f3605 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "clang.cxxflags": [ + "-std=c++17", + "-I${workspaceRoot}/include" + ] +} \ No newline at end of file diff --git a/include/lfhmap.hpp b/include/lfhmap.hpp new file mode 100644 index 0000000..45a6b27 --- /dev/null +++ b/include/lfhmap.hpp @@ -0,0 +1,198 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +/** +Pensé en un mundo sin memoria, sin tiempo; consideré la posibilidad de un lenguaje +que ignorara los sustantivos, un lenguaje de verbos impersonales y de indeclinables +epítetos. Así fueron muriendo los días y con los días los años, pero algo parecido +a la felicidad ocurrió una mañana. Llovió, con lentitud poderosa. +**/ + +namespace mct20 { + + template + class accessor { + public: + accessor(const T* ptr, std::atomic& incremented_ref) + : pointer(ptr) + , reference_cnt(incremented_ref) + { + assert(reference_cnt.load() != 0); + } + + accessor(const accessor& a) + : pointer(a.pointer) + , reference_cnt(a.reference_cnt) + { + reference_cnt.fetch_add(1); + } + + accessor(const accessor&& a) + : pointer(a.pointer) + , reference_cnt(a.reference_cnt) + { + reference_cnt.fetch_add(1); + } + + operator const T&() { + return *pointer; + } + ~accessor() { + reference_cnt.fetch_sub(1); + } + private: + const T* pointer; + std::atomic& reference_cnt; + }; + + namespace _details_ { + #ifdef __cpp_lib_hardware_interference_size + constexpr size_t predictable_padding = std::hardware_constructive_interference_size; + #else + // Wild guess, may be suboptimal or plain wrong + constexpr size_t predictable_padding = 128; + #endif + + size_t rotl(size_t a, uint8_t b) { + b%=sizeof(size_t)*8; + return a << b | a >> (sizeof(size_t)*8 - b); + } + + template + constexpr size_t alignment = + (against%predictable_padding!=0)*predictable_padding + + (against/predictable_padding)*predictable_padding; + + template + class bucket { + public: + bucket() + : start{nullptr} + {} + + void push(size_t hash, const K& key, const V& value) { + auto t = new node{ + .contents = node_contents{ + .key{key}, + .ptr{new V{value}}, + .hash{hash}, + .references{1} + } + }; + t->contents.next.store(t); + node* expect; + do { + expect = start.load(); + t->contents.next.store(expect); + } while( + !std::atomic_compare_exchange_strong( + &start, + &expect, + t + ) + ); + } + + std::optional> get(const size_t hash, const K& key) { + auto v = start.load(); + while(v) { + if(v->contents.references.fetch_add(1)!=0) + { + if(v->contents.hash == hash) { + if(v->contents.key == key) { + return accessor( + v->contents.ptr, + v->contents.references + ); + } else { + auto n = reinterpret_cast(v->contents.next.load()); + v->contents.references.fetch_sub(1); + v = n; + } + } else { + auto n = reinterpret_cast(v->contents.next.load()); + v->contents.references.fetch_sub(1); + v = n; + } + } + else + { + auto n = reinterpret_cast(v->contents.next.load()); + v->contents.references.fetch_sub(1); + v = n; + } + } + return std::nullopt; + } + + struct node_contents{ + std::atomic next; + const K key; + const V* ptr; + size_t hash; + std::atomic references; + }; + + using node = union { + alignas(alignment) node_contents contents; + }; + + using node_ptr = std::atomic; + + node_ptr start; + }; + } + +template> +class lfhmap { + using bucket = _details_::bucket; + +public: + std::optional> get(const K& key) { + auto l = hash{}(key); + auto ret = buckets[l%bucket_count].get(l, key); + if(ret) return ret; + l = _details_::rotl(l, sizeof(size_t)*4); + return buckets[l%bucket_count].get(l, key); + } + + void set(const K& key, const V& value) { + const auto l = hash{}(key); + auto& ref = buckets[l%bucket_count]; + if(ref.start.load() == nullptr) + { + ref.push(l, key, value); + return; + } + const auto l2 = _details_::rotl(l, sizeof(size_t)*4); + auto& ref2 = buckets[l2%bucket_count]; + if(ref2.start.load() == nullptr) + { + ref2.push(l2, key, value); + return; + } + if((l^l2)&1) { + ref.push(l, key, value); + } else { + ref2.push(l2, key, value); + } + return; + } + + lfhmap() { + for(auto& a : buckets) { + a.start = nullptr; + } + } +private: + std::array buckets; +}; + +} \ No newline at end of file diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..2b61989 --- /dev/null +++ b/tests.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +g++ -pthread -Iinclude -std=c++17 -O3 -g tests/test01.cpp +./a.out || echo "FAILURE ON TEST01" + +g++ -pthread -Iinclude -std=c++17 -O3 -g tests/test02.cpp +./a.out || echo "FAILURE ON TEST02" + +g++ -pthread -Iinclude -std=c++17 -O0 -g tests/test03.cpp +./a.out || echo "FAILURE ON TEST03" + +g++ -pthread -Iinclude -std=c++17 -O0 -g tests/test04.cpp +./a.out || echo "FAILURE ON TEST04" \ No newline at end of file diff --git a/tests/test01.cpp b/tests/test01.cpp new file mode 100644 index 0000000..1cb1b9a --- /dev/null +++ b/tests/test01.cpp @@ -0,0 +1,21 @@ +#include "lfhmap.hpp" +#include +#include + +int main() { + size_t v = 13; + auto map = new mct20::lfhmap(); + for(int a = 0; a < 250000; a++) { + //if(a % 1000 == 0) std::cout << a << std::endl; + map->set(v, std::to_string(v)); + if(auto acc = map->get(v); acc) { + const std::string& t = acc.value(); + if(t != std::to_string(v)) + return 1; + } else + return 1; + v*=121; + v+=17; + } + return 0; +} \ No newline at end of file diff --git a/tests/test02.cpp b/tests/test02.cpp new file mode 100644 index 0000000..d105489 --- /dev/null +++ b/tests/test02.cpp @@ -0,0 +1,22 @@ +#include "lfhmap.hpp" +#include +#include +#include + +int main() { + size_t v = 13; + auto map = new mct20::lfhmap(); + auto start = std::chrono::high_resolution_clock::now(); + for(int a = 0; a < 250000; a++) { + map->set(v, v); + const size_t& t = map->get(v).value(); + if(t != v) + return 1; + v*=121; + v+=17; + } + auto time = std::chrono::high_resolution_clock::now() - start; + std::cout << "Test 02 took " << std::chrono::duration_cast(time).count() << "ms" << std::endl; + std::cout << "Per 1R1W " << std::chrono::duration_cast(time).count()/250000 << "ns" << std::endl; + return 0; +} \ No newline at end of file diff --git a/tests/test03.cpp b/tests/test03.cpp new file mode 100644 index 0000000..388a035 --- /dev/null +++ b/tests/test03.cpp @@ -0,0 +1,30 @@ +#include "lfhmap.hpp" +#include +#include +#include + +int main() { + size_t v = 13; + auto map = new mct20::lfhmap(); + for(int a = 0; a < 250000; a++) { + map->set(v, v); + const size_t& t = map->get(v).value(); + if(t != v) + return 1; + v*=121; + v+=17; + } + v = 13; + auto start = std::chrono::high_resolution_clock::now(); + for(int a = 0; a < 250000; a++) { + const size_t& t = map->get(v).value(); + if(t != v) + return 1; + v*=121; + v+=17; + } + auto time = std::chrono::high_resolution_clock::now() - start; + std::cout << "Test 03 took " << std::chrono::duration_cast(time).count() << "ms" << std::endl; + std::cout << "Per 1R " << std::chrono::duration_cast(time).count()/250000 << "ns" << std::endl; + return 0; +} \ No newline at end of file diff --git a/tests/test04.cpp b/tests/test04.cpp new file mode 100644 index 0000000..ed2f39c --- /dev/null +++ b/tests/test04.cpp @@ -0,0 +1,42 @@ +#include "lfhmap.hpp" +#include +#include +#include +#include + +template +void repeat(size_t nb, fn v) { + while(nb--) { + v(); + } +} + +int main() { + constexpr size_t thread_cnt = 16; + size_t v = 0; + auto map = new mct20::lfhmap(); + std::vector> finals; + repeat(thread_cnt, [&](){ + size_t v2 = v; + v++; + finals.push_back(std::async(std::launch::async, [&map, v2](){ + for(int a = v2; a < 250000; a+=thread_cnt) { + map->set(a, std::to_string(a)); + if(auto acc = map->get(a); acc) { + const std::string& t = acc.value(); + if(t != std::to_string(a)) + return 1; + } else + return 1; + } + return 0; + })); + }); + + for(auto& a : finals) a.wait(); + int ret = 0; + for(auto& a : finals) ret += a.get(); + + + return ret; +} \ No newline at end of file