├── requirements.txt ├── contrib ├── ClickHouseHashTable │ ├── version.txt │ ├── CMakeLists.txt │ ├── HashTableAllocator.h │ ├── types.h │ ├── Allocator.cpp │ ├── HashMap.h │ ├── Allocator.h │ ├── Hash.h │ └── HashTable.h └── CMakeLists.txt ├── .gitignore ├── src ├── Utils.h ├── defines.h ├── CMakeLists.txt ├── HashFunctions.h ├── Utils.cpp ├── HashTables.h └── main.cpp ├── cmake ├── glob_sources.cmake ├── arch.cmake ├── tools.cmake └── sanitize.cmake ├── .gitmodules ├── CMakeLists.txt ├── .clang-format ├── load_data.sh ├── .clang-tidy ├── benchmark.py ├── LICENSE ├── README.md └── Results.md /requirements.txt: -------------------------------------------------------------------------------- 1 | prettytable 2 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/version.txt: -------------------------------------------------------------------------------- 1 | ClickHouseHashTable from ClickHouse sources https://github.com/ClickHouse/ClickHouse 2 | Commit 0cab773e7b55c0fbe1c8b3d7d852af0b180ba967 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /build_* 3 | /build-* 4 | 5 | # Python cache 6 | *.pyc 7 | __pycache__ 8 | *.pytest_cache 9 | .mypy_cache 10 | 11 | # clangd cache 12 | /.cache 13 | 14 | /compile_commands.json 15 | 16 | /data 17 | -------------------------------------------------------------------------------- /src/Utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "defines.h" 6 | 7 | size_t getPageSizeInBytes(); 8 | 9 | size_t getCurrentMemoryUsageInBytes(); 10 | 11 | struct Column 12 | { 13 | std::string type; 14 | std::vector data; 15 | }; 16 | 17 | Column readColumnFromFile(std::string_view file_name); 18 | -------------------------------------------------------------------------------- /src/defines.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define NOINLINE __attribute__((noinline)) 7 | 8 | using UInt8 = char8_t; 9 | using UInt16 = uint16_t; 10 | using UInt32 = uint32_t; 11 | using UInt64 = uint64_t; 12 | 13 | using Int8 = int8_t; 14 | using Int16 = int16_t; 15 | using Int32 = int32_t; 16 | using Int64 = int64_t; 17 | -------------------------------------------------------------------------------- /cmake/glob_sources.cmake: -------------------------------------------------------------------------------- 1 | macro(add_glob cur_list) 2 | file(GLOB __tmp CONFIGURE_DEPENDS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${ARGN}) 3 | list(APPEND ${cur_list} ${__tmp}) 4 | endmacro() 5 | 6 | macro(add_headers_and_sources prefix common_path) 7 | add_glob(${prefix}_headers ${common_path}/*.h) 8 | add_glob(${prefix}_sources ${common_path}/*.cpp ${common_path}/*.c) 9 | endmacro() 10 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set (SOURCES 2 | Allocator.cpp) 3 | 4 | set (HEADERS 5 | Hash.h 6 | Allocator.h 7 | HashTableAllocator.h 8 | HashTable.h 9 | HashMap.h) 10 | 11 | add_library(clickhouse_hash_table STATIC) 12 | target_sources(clickhouse_hash_table PRIVATE ${SOURCES} ${HEADERS}) 13 | target_include_directories(clickhouse_hash_table INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/..) 14 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_headers_and_sources(hash_table_aggregation_benchmark ${CMAKE_CURRENT_SOURCE_DIR}) 2 | add_executable(hash_table_aggregation_benchmark ${hash_table_aggregation_benchmark_headers} ${hash_table_aggregation_benchmark_sources}) 3 | target_link_libraries(hash_table_aggregation_benchmark PRIVATE 4 | clickhouse_hash_table 5 | abseil_swiss_tables 6 | hopscotch_map 7 | unordered_dense 8 | flat_hash_map 9 | sparsehash) 10 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/HashTableAllocator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Allocator.h" 4 | 5 | /** 6 | * We are going to use the entire memory we allocated when resizing a hash 7 | * table, so it makes sense to pre-fault the pages so that page faults don't 8 | * interrupt the resize loop. Set the allocator parameter accordingly. 9 | */ 10 | using HashTableAllocator = Allocator; 11 | 12 | template 13 | using HashTableAllocatorWithStackMemory = AllocatorWithStackMemory; 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contrib/abseil-cpp"] 2 | path = contrib/abseil-cpp 3 | url = git@github.com:abseil/abseil-cpp.git 4 | [submodule "contrib/flat_hash_map"] 5 | path = contrib/flat_hash_map 6 | url = git@github.com:skarupke/flat_hash_map.git 7 | [submodule "contrib/hopscotch-map"] 8 | path = contrib/hopscotch-map 9 | url = git@github.com:Tessil/hopscotch-map.git 10 | [submodule "contrib/unordered_dense"] 11 | path = contrib/unordered_dense 12 | url = git@github.com:martinus/unordered_dense.git 13 | [submodule "contrib/sparsehash-c11"] 14 | path = contrib/sparsehash-c11 15 | url = git@github.com:sparsehash/sparsehash-c11.git 16 | -------------------------------------------------------------------------------- /cmake/arch.cmake: -------------------------------------------------------------------------------- 1 | if (CMAKE_SYSTEM_PROCESSOR MATCHES "amd64|x86_64") 2 | if (CMAKE_LIBRARY_ARCHITECTURE MATCHES "i386") 3 | message (FATAL_ERROR "32bit platforms are not supported") 4 | endif () 5 | set (ARCH_AMD64 1) 6 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64.*|AARCH64.*|arm64.*|ARM64.*)") 7 | set (ARCH_AARCH64 1) 8 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc64le.*|ppc64le.*|PPC64LE.*)") 9 | set (ARCH_PPC64LE 1) 10 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(s390x.*|S390X.*)") 11 | set (ARCH_S390X 1) 12 | elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64") 13 | set (ARCH_RISCV64 1) 14 | else () 15 | message (FATAL_ERROR "Platform ${CMAKE_SYSTEM_PROCESSOR} is not supported") 16 | endif () 17 | -------------------------------------------------------------------------------- /cmake/tools.cmake: -------------------------------------------------------------------------------- 1 | # Compiler 2 | 3 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 4 | set (COMPILER_CLANG 1) 5 | else () 6 | message (FATAL_ERROR "Compiler ${CMAKE_CXX_COMPILER_ID} is not supported") 7 | endif () 8 | 9 | # Print details to output 10 | execute_process(COMMAND ${CMAKE_CXX_COMPILER} --version OUTPUT_VARIABLE COMPILER_SELF_IDENTIFICATION OUTPUT_STRIP_TRAILING_WHITESPACE) 11 | message (STATUS "Using compiler:\n${COMPILER_SELF_IDENTIFICATION}") 12 | 13 | # Require minimum compiler versions 14 | set (CLANG_MINIMUM_VERSION 15) 15 | 16 | if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS ${CLANG_MINIMUM_VERSION}) 17 | message (FATAL_ERROR "Compilation with Clang version ${CMAKE_CXX_COMPILER_VERSION} is unsupported, the minimum required version is ${CLANG_MINIMUM_VERSION}.") 18 | endif () 19 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using Int8 = int8_t; 7 | using Int16 = int16_t; 8 | using Int32 = int32_t; 9 | using Int64 = int64_t; 10 | 11 | #ifndef __cpp_char8_t 12 | using char8_t = unsigned char; 13 | #endif 14 | 15 | /// This is needed for more strict aliasing. https://godbolt.org/z/xpJBSb https://stackoverflow.com/a/57453713 16 | using UInt8 = char8_t; 17 | 18 | using UInt16 = uint16_t; 19 | using UInt32 = uint32_t; 20 | using UInt64 = uint64_t; 21 | 22 | using String = std::string; 23 | 24 | namespace DB 25 | { 26 | 27 | using UInt8 = ::UInt8; 28 | using UInt16 = ::UInt16; 29 | using UInt32 = ::UInt32; 30 | using UInt64 = ::UInt64; 31 | 32 | using Int8 = ::Int8; 33 | using Int16 = ::Int16; 34 | using Int32 = ::Int32; 35 | using Int64 = ::Int64; 36 | 37 | using Float32 = float; 38 | using Float64 = double; 39 | 40 | using String = std::string; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/HashFunctions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | template 10 | struct StandardHashFunctionType 11 | { 12 | using Hash = std::hash; 13 | static constexpr std::string_view description = "std::hash"; 14 | }; 15 | 16 | template 17 | struct ClickHouseHashFunctionType 18 | { 19 | using Hash = DefaultHash; 20 | static constexpr std::string_view description = "ClickHouse hash"; 21 | }; 22 | 23 | template 24 | struct AbslHashFunctionType 25 | { 26 | using Hash = absl::Hash; 27 | static constexpr std::string_view description = "absl::Hash"; 28 | }; 29 | 30 | template 31 | void dispatchHashFunctionType(std::string_view hash_function_type, Callback && callback) 32 | { 33 | if (hash_function_type == "std_hash") 34 | callback(StandardHashFunctionType()); 35 | else if (hash_function_type == "ch_hash") 36 | callback(ClickHouseHashFunctionType()); 37 | else if (hash_function_type == "absl_hash") 38 | callback(AbslHashFunctionType()); 39 | else 40 | throw std::runtime_error("Invalid hash function type " + std::string(hash_function_type)); 41 | } 42 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/Allocator.cpp: -------------------------------------------------------------------------------- 1 | #include "Allocator.h" 2 | 3 | #include 4 | #include 5 | 6 | Int64 getPageSize() 7 | { 8 | Int64 page_size = sysconf(_SC_PAGESIZE); 9 | if (page_size < 0) 10 | abort(); 11 | return page_size; 12 | } 13 | 14 | 15 | /** Keep definition of this constant in cpp file; otherwise its value 16 | * is inlined into allocator code making it impossible to override it 17 | * in third-party code. 18 | * 19 | * Note: extern may seem redundant, but is actually needed due to bug in GCC. 20 | * See also: https://gcc.gnu.org/legacy-ml/gcc-help/2017-12/msg00021.html 21 | */ 22 | #ifdef NDEBUG 23 | __attribute__((__weak__)) extern const size_t MMAP_THRESHOLD = 64 * (1ULL << 20); 24 | #else 25 | /** 26 | * In debug build, use small mmap threshold to reproduce more memory 27 | * stomping bugs. Along with ASLR it will hopefully detect more issues than 28 | * ASan. The program may fail due to the limit on number of memory mappings. 29 | * 30 | * Not too small to avoid too quick exhaust of memory mappings. 31 | */ 32 | __attribute__((__weak__)) extern const size_t MMAP_THRESHOLD = 16384; 33 | #endif 34 | 35 | template class Allocator; 36 | template class Allocator; 37 | template class Allocator; 38 | template class Allocator; 39 | -------------------------------------------------------------------------------- /contrib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Add ClickHouseHashTable library 2 | 3 | add_subdirectory(ClickHouseHashTable EXCLUDE_FROM_ALL) 4 | 5 | # Add Abseil swiss tables library 6 | 7 | set(BUILD_TESTING OFF) 8 | set(ABSL_PROPAGATE_CXX_STD ON) 9 | 10 | add_subdirectory(abseil-cpp EXCLUDE_FROM_ALL) 11 | add_library(abseil_swiss_tables INTERFACE) 12 | 13 | target_link_libraries(abseil_swiss_tables INTERFACE 14 | absl::flat_hash_map 15 | absl::flat_hash_set 16 | ) 17 | 18 | get_target_property(FLAT_HASH_MAP_INCLUDE_DIR absl::flat_hash_map INTERFACE_INCLUDE_DIRECTORIES) 19 | target_include_directories(abseil_swiss_tables SYSTEM BEFORE INTERFACE ${FLAT_HASH_MAP_INCLUDE_DIR}) 20 | 21 | get_target_property(FLAT_HASH_SET_INCLUDE_DIR absl::flat_hash_set INTERFACE_INCLUDE_DIRECTORIES) 22 | target_include_directories(abseil_swiss_tables SYSTEM BEFORE INTERFACE ${FLAT_HASH_SET_INCLUDE_DIR}) 23 | 24 | # Add hopscotch-map library 25 | add_subdirectory(hopscotch-map EXCLUDE_FROM_ALL) 26 | 27 | # Add unordered_dense library 28 | add_subdirectory(unordered_dense EXCLUDE_FROM_ALL) 29 | 30 | # Add flat_hash_map library 31 | 32 | set(FLAT_HASH_MAP_HEADERS 33 | flat_hash_map/bytell_hash_map.hpp 34 | flat_hash_map/flat_hash_map.hpp 35 | flat_hash_map/unordered_map.hpp 36 | ) 37 | 38 | add_library(flat_hash_map INTERFACE ${FLAT_HASH_MAP_HEADERS}) 39 | 40 | # Add sparsehash library 41 | add_library(sparsehash INTERFACE) 42 | target_include_directories(sparsehash SYSTEM BEFORE INTERFACE "${PROJECT_SOURCE_DIR}/contrib/sparsehash-c11") 43 | -------------------------------------------------------------------------------- /cmake/sanitize.cmake: -------------------------------------------------------------------------------- 1 | # Possible values: 2 | # - `address` (ASan) 3 | # - `memory` (MSan) 4 | # - `thread` (TSan) 5 | # - `undefined` (UBSan) 6 | # - "" (no sanitizing) 7 | option (SANITIZE "Enable one of the code sanitizers" "") 8 | 9 | set (SAN_FLAGS "${SAN_FLAGS} -g -fno-omit-frame-pointer -DSANITIZER") 10 | 11 | if (SANITIZE) 12 | if (SANITIZE STREQUAL "address") 13 | set (ASAN_FLAGS "-fsanitize=address -fno-sanitize-recover=all -fsanitize-address-use-after-scope") 14 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SAN_FLAGS} ${ASAN_FLAGS}") 15 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SAN_FLAGS} ${ASAN_FLAGS}") 16 | elseif (SANITIZE STREQUAL "memory") 17 | set (MSAN_FLAGS "-fsanitize=memory -fsanitize-memory-use-after-dtor -fsanitize-memory-track-origins -fno-optimize-sibling-calls -fPIC -fpie") 18 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SAN_FLAGS} ${MSAN_FLAGS}") 19 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SAN_FLAGS} ${MSAN_FLAGS}") 20 | elseif (SANITIZE STREQUAL "thread") 21 | set (TSAN_FLAGS "-fsanitize=thread") 22 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SAN_FLAGS} ${TSAN_FLAGS}") 23 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SAN_FLAGS} ${TSAN_FLAGS}") 24 | elseif (SANITIZE STREQUAL "undefined") 25 | set (UBSAN_FLAGS "-fsanitize=undefined -fno-sanitize-recover=all -fno-sanitize=float-divide-by-zero") 26 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${SAN_FLAGS} ${UBSAN_FLAGS}") 27 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${SAN_FLAGS} ${UBSAN_FLAGS}") 28 | else () 29 | message (FATAL_ERROR "Unknown sanitizer type: ${SANITIZE}") 30 | endif () 31 | endif() 32 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | 3 | project (hash-table-aggregation-benchmark LANGUAGES C CXX ASM) 4 | 5 | message (STATUS "Source dir ${PROJECT_SOURCE_DIR}") 6 | 7 | include(cmake/arch.cmake) 8 | include(cmake/glob_sources.cmake) 9 | include(cmake/sanitize.cmake) 10 | 11 | set (CMAKE_EXPORT_COMPILE_COMMANDS ON) 12 | 13 | set (CMAKE_CXX_STANDARD 20) 14 | set (CMAKE_CXX_EXTENSIONS OFF) 15 | set (CMAKE_CXX_STANDARD_REQUIRED ON) 16 | 17 | set (CMAKE_C_STANDARD 11) 18 | set (CMAKE_C_EXTENSIONS ON) 19 | set (CMAKE_C_STANDARD_REQUIRED ON) 20 | 21 | set (COMPILER_FLAGS "${COMPILER_FLAGS} -falign-functions=32 -fno-omit-frame-pointer") 22 | 23 | if (ARCH_AMD64) 24 | set (COMPILER_FLAGS "${COMPILER_FLAGS} -mssse3 -msse4.1 -msse4.2 -mpclmul -mpopcnt -mbranches-within-32B-boundaries") 25 | endif() 26 | 27 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra") 28 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") 29 | set (CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -Wall -Wextra") 30 | 31 | set (DEBUG_INFO_FLAGS "${DEBUG_INFO_FLAGS} -g -gdwarf-4") 32 | 33 | set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILER_FLAGS}") 34 | set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O3 ${DEBUG_INFO_FLAGS} ${CMAKE_CXX_FLAGS_ADD}") 35 | set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 ${DEBUG_INFO_FLAGS} -fno-inline ${CMAKE_CXX_FLAGS_ADD}") 36 | 37 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMPILER_FLAGS} ${CMAKE_C_FLAGS_ADD}") 38 | set (CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -O3 ${DEBUG_INFO_FLAGS} ${CMAKE_C_FLAGS_ADD}") 39 | set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 ${DEBUG_INFO_FLAGS} -fno-inline ${CMAKE_C_FLAGS_ADD}") 40 | 41 | set (CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${COMPILER_FLAGS} ${CMAKE_ASM_FLAGS_ADD}") 42 | set (CMAKE_ASM_FLAGS_RELWITHDEBINFO "${CMAKE_ASM_FLAGS_RELWITHDEBINFO} -O3 ${DEBUG_INFO_FLAGS} ${CMAKE_ASM_FLAGS_ADD}") 43 | set (CMAKE_ASM_FLAGS_DEBUG "${CMAKE_ASM_FLAGS_DEBUG} -O0 ${DEBUG_INFO_FLAGS} -fno-inline ${CMAKE_ASM_FLAGS_ADD}") 44 | 45 | add_subdirectory(contrib) 46 | add_subdirectory(src) 47 | -------------------------------------------------------------------------------- /src/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include "Utils.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace 8 | { 9 | 10 | size_t readVarUInt(UInt64 & x, std::istream & istr) 11 | { 12 | x = 0; 13 | 14 | size_t read_size = 0; 15 | for (size_t i = 0; i < 9; ++i) 16 | { 17 | UInt64 byte = istr.get(); 18 | ++read_size; 19 | 20 | x |= (byte & 0x7F) << (7 * i); 21 | 22 | if (!(byte & 0x80)) 23 | break; 24 | } 25 | 26 | return read_size; 27 | } 28 | 29 | } 30 | 31 | size_t getPageSizeInBytes() 32 | { 33 | Int64 page_size = sysconf(_SC_PAGESIZE); 34 | if (page_size < 0) 35 | return 0; 36 | 37 | return page_size; 38 | } 39 | 40 | size_t getCurrentMemoryUsageInBytes() 41 | { 42 | std::ifstream stream("/proc/self/statm", std::ios_base::in); 43 | 44 | if (!stream.is_open()) 45 | return 0; 46 | 47 | Int64 ignore = 0; 48 | Int64 rss = 0; 49 | 50 | stream >> ignore; 51 | stream >> rss; 52 | 53 | return rss * getPageSizeInBytes(); 54 | } 55 | 56 | Column readColumnFromFile(std::string_view file_name) 57 | { 58 | std::ifstream stream(std::string(file_name), std::ios_base::binary); 59 | 60 | stream.seekg(0, std::ios::end); 61 | size_t file_size = stream.tellg(); 62 | 63 | stream.seekg(0, std::ios::beg); 64 | 65 | size_t read_bytes_size = 0; 66 | 67 | UInt64 number_of_columns = 0; 68 | read_bytes_size += readVarUInt(number_of_columns, stream); 69 | if (number_of_columns != 1) 70 | throw std::runtime_error("Invalid number of columns. Expected 1 column."); 71 | 72 | UInt64 column_name_length = 0; 73 | read_bytes_size += readVarUInt(column_name_length, stream); 74 | 75 | std::string column_name(column_name_length, ' '); 76 | stream.read(column_name.data(), column_name_length); 77 | read_bytes_size += column_name_length; 78 | 79 | UInt64 type_name_length = 0; 80 | read_bytes_size += readVarUInt(type_name_length, stream); 81 | 82 | std::string type_name(type_name_length, ' '); 83 | stream.read(type_name.data(), type_name_length); 84 | read_bytes_size += type_name_length; 85 | 86 | size_t column_data_size = file_size - read_bytes_size; 87 | 88 | std::vector data(column_data_size); 89 | stream.read(data.data(), column_data_size); 90 | 91 | return Column{std::move(type_name), std::move(data)}; 92 | } 93 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | Language: Cpp 3 | AlignAfterOpenBracket: AlwaysBreak 4 | BreakBeforeBraces: Custom 5 | BraceWrapping: 6 | AfterClass: true 7 | AfterControlStatement: true 8 | AfterEnum: true 9 | AfterFunction: true 10 | AfterNamespace: true 11 | AfterStruct: true 12 | AfterUnion: true 13 | BeforeCatch: true 14 | BeforeElse: true 15 | BeforeLambdaBody: true 16 | IndentBraces: false 17 | BreakConstructorInitializersBeforeComma: false 18 | Cpp11BracedListStyle: true 19 | ColumnLimit: 140 20 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 21 | ExperimentalAutoDetectBinPacking: true 22 | UseTab: Never 23 | TabWidth: 4 24 | Standard: Cpp11 25 | PointerAlignment: Middle 26 | MaxEmptyLinesToKeep: 2 27 | KeepEmptyLinesAtTheStartOfBlocks: false 28 | AllowShortFunctionsOnASingleLine: InlineOnly 29 | AlwaysBreakTemplateDeclarations: true 30 | IndentCaseLabels: true 31 | SpaceAfterTemplateKeyword: true 32 | SpaceBeforeCpp11BracedList: false 33 | SortIncludes: true 34 | IndentPPDirectives: AfterHash 35 | IncludeCategories: 36 | - Regex: '^<[a-z_]+>' 37 | Priority: 1 38 | - Regex: '^<[a-z_]+.h>' 39 | Priority: 2 40 | - Regex: '^["<](common|ext|mysqlxx|daemon|zkutil)/' 41 | Priority: 90 42 | - Regex: '^["<](DB)/' 43 | Priority: 100 44 | - Regex: '^["<](Poco)/' 45 | Priority: 50 46 | - Regex: '^"' 47 | Priority: 110 48 | - Regex: '/' 49 | Priority: 30 50 | - Regex: '.*' 51 | Priority: 40 52 | ReflowComments: false 53 | AlignEscapedNewlinesLeft: false 54 | AlignEscapedNewlines: DontAlign 55 | AlignTrailingComments: false 56 | 57 | # Not changed: 58 | AccessModifierOffset: -4 59 | AlignConsecutiveAssignments: false 60 | AlignOperands: false 61 | AllowAllParametersOfDeclarationOnNextLine: true 62 | AllowShortBlocksOnASingleLine: false 63 | AllowShortCaseLabelsOnASingleLine: false 64 | AllowShortIfStatementsOnASingleLine: false 65 | AllowShortLoopsOnASingleLine: false 66 | AlwaysBreakAfterDefinitionReturnType: None 67 | AlwaysBreakBeforeMultilineStrings: false 68 | BinPackArguments: false 69 | BinPackParameters: false 70 | BreakBeforeBinaryOperators: All 71 | BreakBeforeTernaryOperators: true 72 | CommentPragmas: '^ IWYU pragma:' 73 | ConstructorInitializerIndentWidth: 4 74 | ContinuationIndentWidth: 4 75 | DerivePointerAlignment: false 76 | DisableFormat: false 77 | IndentRequiresClause: false 78 | IndentWidth: 4 79 | IndentWrappedFunctionNames: false 80 | MacroBlockBegin: '' 81 | MacroBlockEnd: '' 82 | NamespaceIndentation: None 83 | ObjCBlockIndentWidth: 4 84 | ObjCSpaceAfterProperty: true 85 | ObjCSpaceBeforeProtocolList: true 86 | PenaltyBreakBeforeFirstCallParameter: 19 87 | PenaltyBreakComment: 300 88 | PenaltyBreakFirstLessLess: 120 89 | PenaltyBreakString: 1000 90 | PenaltyExcessCharacter: 1000000 91 | PenaltyReturnTypeOnItsOwnLine: 60 92 | RemoveBracesLLVM: true 93 | SpaceAfterCStyleCast: false 94 | SpaceBeforeAssignmentOperators: true 95 | SpaceBeforeParens: ControlStatements 96 | SpaceInEmptyParentheses: false 97 | SpacesBeforeTrailingComments: 1 98 | SpacesInContainerLiterals: true 99 | SpacesInCStyleCastParentheses: false 100 | SpacesInParentheses: false 101 | SpacesInSquareBrackets: false 102 | -------------------------------------------------------------------------------- /src/HashTables.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "defines.h" 15 | 16 | template 17 | struct ClickHouseHashTableType 18 | { 19 | using HashTable = HashMap; 20 | static constexpr std::string_view description = "ClickHouse HashMap"; 21 | static constexpr bool has_initialization = false; 22 | }; 23 | 24 | template 25 | struct AbseilHashTableType 26 | { 27 | using HashTable = ::absl::flat_hash_map; 28 | static constexpr std::string_view description = "absl::flat_hash_map"; 29 | static constexpr bool has_initialization = false; 30 | }; 31 | 32 | template 33 | struct DenseHashTableType 34 | { 35 | using HashTable = ::google::dense_hash_map; 36 | static constexpr std::string_view description = "google::dense_hash_map"; 37 | static constexpr bool has_initialization = true; 38 | 39 | static void initialize(HashTable & hash_table) { hash_table.set_empty_key(std::numeric_limits::max()); } 40 | }; 41 | 42 | template 43 | struct TslHopscotchHashTableType 44 | { 45 | using HashTable = tsl::hopscotch_map; 46 | static constexpr std::string_view description = "tsl::hopscotch_map"; 47 | static constexpr bool has_initialization = false; 48 | }; 49 | 50 | template 51 | struct AnkerlUnorderedDenseHashTableType 52 | { 53 | using HashTable = ankerl::unordered_dense::map; 54 | static constexpr std::string_view description = "ankerl::unordered_dense::map"; 55 | static constexpr bool has_initialization = false; 56 | }; 57 | 58 | template 59 | struct SkaFlatHashTableType 60 | { 61 | using HashTable = ska::flat_hash_map; 62 | static constexpr std::string_view description = "ska::flat_hash_map"; 63 | static constexpr bool has_initialization = false; 64 | }; 65 | 66 | template 67 | struct SkaBytellHashTableType 68 | { 69 | using HashTable = ska::bytell_hash_map; 70 | static constexpr std::string_view description = "ska::bytell_hash_map"; 71 | static constexpr bool has_initialization = false; 72 | }; 73 | 74 | template 75 | struct StandardHashTableType 76 | { 77 | using HashTable = std::unordered_map; 78 | static constexpr std::string_view description = "std::unordered_map"; 79 | static constexpr bool has_initialization = false; 80 | }; 81 | 82 | template 83 | void dispatchHashTableType(std::string_view hash_table_type, Callback && callback) 84 | { 85 | if (hash_table_type == "ch_hash_map") 86 | callback(ClickHouseHashTableType()); 87 | else if (hash_table_type == "absl_hash_map") 88 | callback(AbseilHashTableType()); 89 | else if (hash_table_type == "google_dense_hash_map") 90 | callback(DenseHashTableType()); 91 | else if (hash_table_type == "tsl_hopscotch_hash_map") 92 | callback(TslHopscotchHashTableType()); 93 | else if (hash_table_type == "ankerl_unordered_dense_hash_map") 94 | callback(AnkerlUnorderedDenseHashTableType()); 95 | else if (hash_table_type == "ska_flat_hash_map") 96 | callback(SkaFlatHashTableType()); 97 | else if (hash_table_type == "ska_bytell_hash_map") 98 | callback(SkaBytellHashTableType()); 99 | else if (hash_table_type == "std_hash_map") 100 | callback(StandardHashTableType()); 101 | else 102 | throw std::runtime_error("Invalid hash table type " + std::string(hash_table_type)); 103 | } -------------------------------------------------------------------------------- /load_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p data 4 | cd data 5 | 6 | echo "Download hits.parquet file" 7 | wget "https://datasets.clickhouse.com/hits_compatible/hits.parquet" 8 | 9 | echo "Download clickhouse-local" 10 | curl https://clickhouse.com/ | sh 11 | 12 | ./clickhouse local --multiquery "CREATE TABLE hits 13 | ( 14 | WatchID BIGINT NOT NULL, 15 | JavaEnable SMALLINT NOT NULL, 16 | Title TEXT NOT NULL, 17 | GoodEvent SMALLINT NOT NULL, 18 | EventTime TIMESTAMP NOT NULL, 19 | EventDate Date NOT NULL, 20 | CounterID INTEGER NOT NULL, 21 | ClientIP INTEGER NOT NULL, 22 | RegionID INTEGER NOT NULL, 23 | UserID BIGINT NOT NULL, 24 | CounterClass SMALLINT NOT NULL, 25 | OS SMALLINT NOT NULL, 26 | UserAgent SMALLINT NOT NULL, 27 | URL TEXT NOT NULL, 28 | Referer TEXT NOT NULL, 29 | IsRefresh SMALLINT NOT NULL, 30 | RefererCategoryID SMALLINT NOT NULL, 31 | RefererRegionID INTEGER NOT NULL, 32 | URLCategoryID SMALLINT NOT NULL, 33 | URLRegionID INTEGER NOT NULL, 34 | ResolutionWidth SMALLINT NOT NULL, 35 | ResolutionHeight SMALLINT NOT NULL, 36 | ResolutionDepth SMALLINT NOT NULL, 37 | FlashMajor SMALLINT NOT NULL, 38 | FlashMinor SMALLINT NOT NULL, 39 | FlashMinor2 TEXT NOT NULL, 40 | NetMajor SMALLINT NOT NULL, 41 | NetMinor SMALLINT NOT NULL, 42 | UserAgentMajor SMALLINT NOT NULL, 43 | UserAgentMinor VARCHAR(255) NOT NULL, 44 | CookieEnable SMALLINT NOT NULL, 45 | JavascriptEnable SMALLINT NOT NULL, 46 | IsMobile SMALLINT NOT NULL, 47 | MobilePhone SMALLINT NOT NULL, 48 | MobilePhoneModel TEXT NOT NULL, 49 | Params TEXT NOT NULL, 50 | IPNetworkID INTEGER NOT NULL, 51 | TraficSourceID SMALLINT NOT NULL, 52 | SearchEngineID SMALLINT NOT NULL, 53 | SearchPhrase TEXT NOT NULL, 54 | AdvEngineID SMALLINT NOT NULL, 55 | IsArtifical SMALLINT NOT NULL, 56 | WindowClientWidth SMALLINT NOT NULL, 57 | WindowClientHeight SMALLINT NOT NULL, 58 | ClientTimeZone SMALLINT NOT NULL, 59 | ClientEventTime TIMESTAMP NOT NULL, 60 | SilverlightVersion1 SMALLINT NOT NULL, 61 | SilverlightVersion2 SMALLINT NOT NULL, 62 | SilverlightVersion3 INTEGER NOT NULL, 63 | SilverlightVersion4 SMALLINT NOT NULL, 64 | PageCharset TEXT NOT NULL, 65 | CodeVersion INTEGER NOT NULL, 66 | IsLink SMALLINT NOT NULL, 67 | IsDownload SMALLINT NOT NULL, 68 | IsNotBounce SMALLINT NOT NULL, 69 | FUniqID BIGINT NOT NULL, 70 | OriginalURL TEXT NOT NULL, 71 | HID INTEGER NOT NULL, 72 | IsOldCounter SMALLINT NOT NULL, 73 | IsEvent SMALLINT NOT NULL, 74 | IsParameter SMALLINT NOT NULL, 75 | DontCountHits SMALLINT NOT NULL, 76 | WithHash SMALLINT NOT NULL, 77 | HitColor CHAR NOT NULL, 78 | LocalEventTime TIMESTAMP NOT NULL, 79 | Age SMALLINT NOT NULL, 80 | Sex SMALLINT NOT NULL, 81 | Income SMALLINT NOT NULL, 82 | Interests SMALLINT NOT NULL, 83 | Robotness SMALLINT NOT NULL, 84 | RemoteIP INTEGER NOT NULL, 85 | WindowName INTEGER NOT NULL, 86 | OpenerName INTEGER NOT NULL, 87 | HistoryLength SMALLINT NOT NULL, 88 | BrowserLanguage TEXT NOT NULL, 89 | BrowserCountry TEXT NOT NULL, 90 | SocialNetwork TEXT NOT NULL, 91 | SocialAction TEXT NOT NULL, 92 | HTTPError SMALLINT NOT NULL, 93 | SendTiming INTEGER NOT NULL, 94 | DNSTiming INTEGER NOT NULL, 95 | ConnectTiming INTEGER NOT NULL, 96 | ResponseStartTiming INTEGER NOT NULL, 97 | ResponseEndTiming INTEGER NOT NULL, 98 | FetchTiming INTEGER NOT NULL, 99 | SocialSourceNetworkID SMALLINT NOT NULL, 100 | SocialSourcePage TEXT NOT NULL, 101 | ParamPrice BIGINT NOT NULL, 102 | ParamOrderID TEXT NOT NULL, 103 | ParamCurrency TEXT NOT NULL, 104 | ParamCurrencyID SMALLINT NOT NULL, 105 | OpenstatServiceName TEXT NOT NULL, 106 | OpenstatCampaignID TEXT NOT NULL, 107 | OpenstatAdID TEXT NOT NULL, 108 | OpenstatSourceID TEXT NOT NULL, 109 | UTMSource TEXT NOT NULL, 110 | UTMMedium TEXT NOT NULL, 111 | UTMCampaign TEXT NOT NULL, 112 | UTMContent TEXT NOT NULL, 113 | UTMTerm TEXT NOT NULL, 114 | FromTag TEXT NOT NULL, 115 | HasGCLID SMALLINT NOT NULL, 116 | RefererHash BIGINT NOT NULL, 117 | URLHash BIGINT NOT NULL, 118 | CLID INTEGER NOT NULL 119 | ) 120 | ENGINE = File(Parquet, 'hits.parquet'); 121 | 122 | INSERT INTO TABLE FUNCTION file('WatchID.bin', RowBinaryWithNamesAndTypes) SELECT WatchID FROM hits; 123 | INSERT INTO TABLE FUNCTION file('URLHash.bin', RowBinaryWithNamesAndTypes) SELECT URLHash FROM hits; 124 | INSERT INTO TABLE FUNCTION file('UserID.bin', RowBinaryWithNamesAndTypes) SELECT UserID FROM hits; 125 | INSERT INTO TABLE FUNCTION file('RegionID.bin', RowBinaryWithNamesAndTypes) SELECT RegionID FROM hits; 126 | INSERT INTO TABLE FUNCTION file('CounterID.bin', RowBinaryWithNamesAndTypes) SELECT CounterID FROM hits; 127 | INSERT INTO TABLE FUNCTION file('TraficSourceID.bin', RowBinaryWithNamesAndTypes) SELECT TraficSourceID FROM hits; 128 | INSERT INTO TABLE FUNCTION file('AdvEngineID.bin', RowBinaryWithNamesAndTypes) SELECT AdvEngineID FROM hits; 129 | " 130 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "HashFunctions.h" 7 | #include "HashTables.h" 8 | #include "Utils.h" 9 | #include "defines.h" 10 | 11 | template 12 | struct VoidInitialization 13 | { 14 | void operator()(const Map & map) { (void)(map); } 15 | }; 16 | 17 | template > 18 | void NOINLINE test(const Key * data, size_t size, std::string_view hash_table, std::string_view hash_function, InitFunc init_func = {}) 19 | { 20 | auto start = std::chrono::steady_clock::now(); 21 | size_t map_size = 0; 22 | size_t memory_usage = getCurrentMemoryUsageInBytes(); 23 | 24 | { 25 | Map map; 26 | init_func(map); 27 | 28 | const auto * end = data + size; 29 | for (const auto * current = data; current < end; ++current) 30 | ++map[*current]; 31 | 32 | memory_usage = std::max(getCurrentMemoryUsageInBytes() - memory_usage, getPageSizeInBytes()); 33 | map_size = map.size(); 34 | } 35 | 36 | auto finish = std::chrono::steady_clock::now(); 37 | auto duration = std::chrono::duration_cast(finish - start); 38 | double elapsed_seconds = static_cast(duration.count()) / 1000000000ULL; 39 | 40 | std::cout << "Hash table: " << hash_table << '\n'; 41 | std::cout << "Hash function: " << hash_function << '\n'; 42 | std::cout << "Hash table size: " << map_size << '\n'; 43 | 44 | std::cout << "Elapsed: " << elapsed_seconds << " (" << static_cast(size / elapsed_seconds) << " elem/sec.) " << '\n'; 45 | std::cout << "Memory usage: " << memory_usage << "\n"; 46 | } 47 | 48 | template 49 | static void NOINLINE 50 | testForHashMapType(std::string_view hash_table_type, std::string_view hash_function_type, const Key * data, size_t size) 51 | { 52 | dispatchHashFunctionType( 53 | hash_function_type, 54 | [&](auto && hash_function_type) 55 | { 56 | using HashFunctionType = std::decay_t; 57 | using Hash = typename HashFunctionType::Hash; 58 | 59 | dispatchHashTableType( 60 | hash_table_type, 61 | [&](auto && hash_table_type) 62 | { 63 | using HashTableType = std::decay_t; 64 | using HashTable = typename HashTableType::HashTable; 65 | 66 | if constexpr (HashTableType::has_initialization) 67 | test( 68 | data, size, hash_table_type.description, hash_function_type.description, HashTableType::initialize); 69 | else 70 | test(data, size, hash_table_type.description, hash_function_type.description); 71 | }); 72 | }); 73 | } 74 | 75 | template 76 | static void NOINLINE testForKeyType( 77 | std::string_view hash_table_type, 78 | std::string_view hash_function_type, 79 | std::string_view file_name, 80 | std::string_view key_type_name, 81 | const std::vector & data) 82 | { 83 | size_t size = data.size(); 84 | if (size % sizeof(Key) != 0) 85 | throw std::runtime_error( 86 | "Invalid column data size. Column data size must be divisible by type size " + std::to_string(sizeof(Key))); 87 | 88 | size_t elements_size = size / sizeof(Key); 89 | auto * data_typed = reinterpret_cast(data.data()); 90 | 91 | std::cout << "Hash table type: " << hash_table_type << '\n'; 92 | std::cout << "Hash function type: " << hash_function_type << '\n'; 93 | std::cout << "Key type name: " << key_type_name << '\n'; 94 | std::cout << "File name: " << file_name << '\n'; 95 | std::cout << "Keys size: " << elements_size << '\n'; 96 | 97 | testForHashMapType(hash_table_type, hash_function_type, data_typed, elements_size); 98 | } 99 | 100 | static void runBenchmark(std::string_view hash_table_type, std::string_view hash_function_type, std::string_view file_name) 101 | { 102 | auto column = readColumnFromFile(file_name); 103 | 104 | if (column.type == "UInt8") 105 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 106 | else if (column.type == "UInt16") 107 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 108 | else if (column.type == "UInt32") 109 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 110 | else if (column.type == "UInt64") 111 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 112 | else if (column.type == "Int8") 113 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 114 | else if (column.type == "Int16") 115 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 116 | else if (column.type == "Int32") 117 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 118 | else if (column.type == "Int64") 119 | testForKeyType(hash_table_type, hash_function_type, file_name, column.type, column.data); 120 | else 121 | throw std::runtime_error("Unexpected column type " + column.type + " passed"); 122 | } 123 | 124 | int main(int argc, char ** argv) 125 | { 126 | if (argc < 4) 127 | { 128 | std::cerr << "Usage: hash_table_aggregation_benchmark hash_table_type hash_function_type file_name\n"; 129 | return 1; 130 | } 131 | 132 | std::string hash_table_type = argv[1]; 133 | std::string hash_function_type = argv[2]; 134 | std::string file_name = argv[3]; 135 | 136 | try 137 | { 138 | runBenchmark(hash_table_type, hash_function_type, file_name); 139 | } 140 | catch (const std::exception & ex) 141 | { 142 | std::cerr << ex.what() << '\n'; 143 | return 1; 144 | } 145 | 146 | return 0; 147 | } 148 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | HeaderFilterRegex: '^.*/(base)/.*(h|hpp)$' 2 | 3 | Checks: '*, 4 | -abseil-*, 5 | 6 | -altera-*, 7 | 8 | -android-*, 9 | 10 | -bugprone-assignment-in-if-condition, 11 | -bugprone-branch-clone, 12 | -bugprone-easily-swappable-parameters, 13 | -bugprone-exception-escape, 14 | -bugprone-implicit-widening-of-multiplication-result, 15 | -bugprone-narrowing-conversions, 16 | -bugprone-not-null-terminated-result, 17 | -bugprone-reserved-identifier, 18 | -bugprone-unchecked-optional-access, 19 | -bugprone-*, -- category temporarily disabled because some check(s) in it are slow 20 | 21 | -cert-dcl16-c, 22 | -cert-dcl37-c, 23 | -cert-dcl51-cpp, 24 | -cert-err58-cpp, 25 | -cert-msc32-c, 26 | -cert-msc51-cpp, 27 | -cert-oop54-cpp, 28 | -cert-oop57-cpp, 29 | 30 | -clang-analyzer-optin.performance.Padding, 31 | -clang-analyzer-optin.portability.UnixAPI, 32 | -clang-analyzer-security.insecureAPI.bzero, 33 | -clang-analyzer-security.insecureAPI.strcpy, 34 | -clang-analyzer-*, -- category temporarily disabled because some check(s) in it are slow 35 | 36 | -cppcoreguidelines-avoid-c-arrays, 37 | -cppcoreguidelines-avoid-const-or-ref-data-members, 38 | -cppcoreguidelines-avoid-do-while, 39 | -cppcoreguidelines-avoid-goto, 40 | -cppcoreguidelines-avoid-magic-numbers, 41 | -cppcoreguidelines-avoid-non-const-global-variables, 42 | -cppcoreguidelines-explicit-virtual-functions, 43 | -cppcoreguidelines-init-variables, 44 | -cppcoreguidelines-interfaces-global-init, 45 | -cppcoreguidelines-macro-usage, 46 | -cppcoreguidelines-narrowing-conversions, 47 | -cppcoreguidelines-no-malloc, 48 | -cppcoreguidelines-non-private-member-variables-in-classes, 49 | -cppcoreguidelines-owning-memory, 50 | -cppcoreguidelines-prefer-member-initializer, 51 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 52 | -cppcoreguidelines-pro-bounds-constant-array-index, 53 | -cppcoreguidelines-pro-bounds-pointer-arithmetic, 54 | -cppcoreguidelines-pro-type-const-cast, 55 | -cppcoreguidelines-pro-type-cstyle-cast, 56 | -cppcoreguidelines-pro-type-member-init, 57 | -cppcoreguidelines-pro-type-reinterpret-cast, 58 | -cppcoreguidelines-pro-type-static-cast-downcast, 59 | -cppcoreguidelines-pro-type-union-access, 60 | -cppcoreguidelines-pro-type-vararg, 61 | -cppcoreguidelines-slicing, 62 | -cppcoreguidelines-special-member-functions, 63 | -cppcoreguidelines-*, -- category temporarily disabled because some check(s) in it are slow 64 | 65 | -darwin-*, 66 | 67 | -fuchsia-*, 68 | 69 | -google-build-using-namespace, 70 | -google-readability-braces-around-statements, 71 | -google-readability-casting, 72 | -google-readability-function-size, 73 | -google-readability-namespace-comments, 74 | -google-readability-todo, 75 | -google-upgrade-googletest-case, 76 | 77 | -hicpp-avoid-c-arrays, 78 | -hicpp-avoid-goto, 79 | -hicpp-braces-around-statements, 80 | -hicpp-explicit-conversions, 81 | -hicpp-function-size, 82 | -hicpp-member-init, 83 | -hicpp-move-const-arg, 84 | -hicpp-multiway-paths-covered, 85 | -hicpp-named-parameter, 86 | -hicpp-no-array-decay, 87 | -hicpp-no-assembler, 88 | -hicpp-no-malloc, 89 | -hicpp-signed-bitwise, 90 | -hicpp-special-member-functions, 91 | -hicpp-uppercase-literal-suffix, 92 | -hicpp-use-auto, 93 | -hicpp-use-emplace, 94 | -hicpp-vararg, 95 | 96 | -linuxkernel-*, 97 | 98 | -llvm-*, 99 | 100 | -llvmlibc-*, 101 | 102 | -openmp-*, 103 | 104 | -misc-const-correctness, 105 | -misc-include-cleaner, # useful but far too many occurrences 106 | -misc-no-recursion, 107 | -misc-non-private-member-variables-in-classes, 108 | -misc-confusable-identifiers, # useful but slooow 109 | -misc-use-anonymous-namespace, 110 | 111 | -modernize-avoid-c-arrays, 112 | -modernize-concat-nested-namespaces, 113 | -modernize-macro-to-enum, 114 | -modernize-pass-by-value, 115 | -modernize-return-braced-init-list, 116 | -modernize-use-auto, 117 | -modernize-use-default-member-init, 118 | -modernize-use-emplace, 119 | -modernize-use-nodiscard, 120 | -modernize-use-override, 121 | -modernize-use-trailing-return-type, 122 | 123 | -performance-inefficient-string-concatenation, 124 | -performance-no-int-to-ptr, 125 | -performance-avoid-endl, 126 | -performance-unnecessary-value-param, 127 | 128 | -portability-simd-intrinsics, 129 | 130 | -readability-avoid-unconditional-preprocessor-if, 131 | -readability-braces-around-statements, 132 | -readability-convert-member-functions-to-static, 133 | -readability-else-after-return, 134 | -readability-function-cognitive-complexity, 135 | -readability-function-size, 136 | -readability-identifier-length, 137 | -readability-identifier-naming, # useful but too slow 138 | -readability-implicit-bool-conversion, 139 | -readability-isolate-declaration, 140 | -readability-magic-numbers, 141 | -readability-named-parameter, 142 | -readability-redundant-declaration, 143 | -readability-simplify-boolean-expr, 144 | -readability-static-accessed-through-instance, 145 | -readability-suspicious-call-argument, 146 | -readability-uppercase-literal-suffix, 147 | -readability-use-anyofallof, 148 | 149 | -zircon-*, 150 | ' 151 | 152 | WarningsAsErrors: '*' 153 | 154 | ExtraArgs: 155 | - '-Wno-unknown-pragmas' 156 | - '-Wno-unused-command-line-argument' # similar issue 157 | 158 | CheckOptions: 159 | readability-identifier-naming.ClassCase: CamelCase 160 | readability-identifier-naming.EnumCase: CamelCase 161 | readability-identifier-naming.LocalVariableCase: lower_case 162 | readability-identifier-naming.StaticConstantCase: aNy_CasE 163 | readability-identifier-naming.MemberCase: lower_case 164 | readability-identifier-naming.PrivateMemberPrefix: '' 165 | readability-identifier-naming.ProtectedMemberPrefix: '' 166 | readability-identifier-naming.PublicMemberCase: lower_case 167 | readability-identifier-naming.MethodCase: camelBack 168 | readability-identifier-naming.PrivateMethodPrefix: '' 169 | readability-identifier-naming.ProtectedMethodPrefix: '' 170 | readability-identifier-naming.ParameterPackCase: lower_case 171 | readability-identifier-naming.StructCase: CamelCase 172 | readability-identifier-naming.TemplateTemplateParameterCase: CamelCase 173 | readability-identifier-naming.TemplateParameterCase: lower_case 174 | readability-identifier-naming.TypeTemplateParameterCase: CamelCase 175 | readability-identifier-naming.TypedefCase: CamelCase 176 | readability-identifier-naming.UnionCase: CamelCase 177 | modernize-loop-convert.UseCxx20ReverseRanges: false 178 | performance-move-const-arg.CheckTriviallyCopyableMove: false 179 | readability-identifier-naming.TypeTemplateParameterIgnoredRegexp: expr-type 180 | cppcoreguidelines-avoid-do-while.IgnoreMacros: true 181 | -------------------------------------------------------------------------------- /benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import sys 5 | import shutil 6 | import argparse 7 | import statistics 8 | from prettytable import PrettyTable 9 | 10 | BENCHMARK_EXECUTABLE = "hash_table_aggregation_benchmark" 11 | 12 | MEASURE_RUNS = 1 13 | 14 | FILES = [ 15 | "data/WatchID.bin", 16 | "data/URLHash.bin", 17 | "data/UserID.bin", 18 | "data/RegionID.bin", 19 | "data/CounterID.bin", 20 | "data/TraficSourceID.bin", 21 | "data/AdvEngineID.bin", 22 | ] 23 | 24 | HASH_FUNCTIONS = ["std_hash", "ch_hash", "absl_hash"] 25 | 26 | HASH_TABLES = [ 27 | "ch_hash_map", 28 | "absl_hash_map", 29 | "google_dense_hash_map", 30 | "tsl_hopscotch_hash_map", 31 | "ankerl_unordered_dense_hash_map", 32 | "ska_flat_hash_map", 33 | "ska_bytell_hash_map", 34 | "std_hash_map", 35 | ] 36 | 37 | 38 | def format_readable_size(bytes): 39 | unit_prefixes = ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"] 40 | suffix = "B" 41 | 42 | for unit_prefix in unit_prefixes[:-1]: 43 | if abs(bytes) < 1024.0: 44 | return f"{bytes:.2f} {unit_prefix}{suffix}" 45 | 46 | bytes /= 1024.0 47 | 48 | return f"{bytes:.2f} {unit_prefixes[len(unit_prefixes) - 1]}{suffix}" 49 | 50 | 51 | def parse_argument(argument): 52 | return argument.replace(" ", "").split(",") 53 | 54 | 55 | if __name__ == "__main__": 56 | parser = argparse.ArgumentParser( 57 | prog="HashTableAggregationBenchmark runner", 58 | description="Runs hash table aggregation benchmark", 59 | ) 60 | parser.add_argument( 61 | "--hash-tables", 62 | "-ht", 63 | default=", ".join(HASH_TABLES), 64 | help="Hash tables to benchmark", 65 | ) 66 | parser.add_argument( 67 | "--hash-functions", 68 | "-hf", 69 | default=", ".join(HASH_FUNCTIONS), 70 | help="Hash functions to benchmark", 71 | ) 72 | parser.add_argument( 73 | "--files", "-f", default=", ".join(FILES), help="Files for benchmark" 74 | ) 75 | parser.add_argument( 76 | "--runs", "-r", type=int, default=MEASURE_RUNS, help="Number of measure runs" 77 | ) 78 | parser.add_argument( 79 | "--debug", action="store_true", help="Run benchmark in debug mode" 80 | ) 81 | 82 | args = parser.parse_args() 83 | 84 | hash_tables = parse_argument(args.hash_tables) 85 | hash_functions = parse_argument(args.hash_functions) 86 | files = parse_argument(args.files) 87 | runs = args.runs 88 | debug = args.debug 89 | 90 | def print_debug(*args, **kwargs): 91 | if debug: 92 | print(*args, **kwargs) 93 | 94 | print_debug( 95 | f"Hash tables {hash_tables} hash functions {hash_functions} files {files} runs {runs} debug {debug}" 96 | ) 97 | 98 | if not hash_tables: 99 | print("Invalid input empty hash tables", file=sys.stderr) 100 | exit(-1) 101 | 102 | if not hash_functions: 103 | print("Invalid input empty hash functions", file=sys.stderr) 104 | exit(-1) 105 | 106 | if not files: 107 | print("Invalid input empty files", file=sys.stderr) 108 | exit(-1) 109 | 110 | if runs <= 0: 111 | print("Invalid runs value, expected to be positive integer", file=sys.stderr) 112 | exit(-1) 113 | 114 | if shutil.which(BENCHMARK_EXECUTABLE) is None: 115 | print(f"{BENCHMARK_EXECUTABLE} was not found in PATH") 116 | 117 | for file in files: 118 | key_type_name = None 119 | keys_size = None 120 | hash_table_max_keys_size = None 121 | results = [] 122 | 123 | for hash_table in hash_tables: 124 | for hash_function in hash_functions: 125 | hash_table_name = None 126 | hash_function_name = None 127 | elapsed_seconds_values = [] 128 | memory_usage_in_bytes_values = [] 129 | 130 | for _ in range(0, runs): 131 | cmd = f"{BENCHMARK_EXECUTABLE} {hash_table} {hash_function} {file}" 132 | output = subprocess.check_output(cmd, shell=True).decode() 133 | 134 | print_debug(f"Output\n{output}") 135 | 136 | lines = output.split("\n") 137 | 138 | elapsed_seconds = None 139 | memory_usage_in_bytes = None 140 | 141 | for line in lines: 142 | if not line: 143 | continue 144 | 145 | parts = line.split(": ") 146 | key = parts[0] 147 | value = parts[1] 148 | 149 | if key == "Key type name": 150 | key_type_name = value 151 | elif key == "Keys size": 152 | keys_size = int(value) 153 | elif key == "Hash table": 154 | hash_table_name = value 155 | elif key == "Hash function": 156 | hash_function_name = value 157 | elif key == "Hash table size": 158 | hash_table_size = int(value) 159 | 160 | if hash_table_max_keys_size is None: 161 | hash_table_max_keys_size = hash_table_size 162 | 163 | if hash_table_size != hash_table_max_keys_size: 164 | print( 165 | f"Invalid hash map size for hash table {hash_table} with hash {hash_function}", 166 | file=sys.stderr, 167 | ) 168 | 169 | hash_table_max_keys_size = max( 170 | hash_table_size, hash_table_max_keys_size 171 | ) 172 | elif key == "Elapsed": 173 | elapsed_seconds = float(value.split(" ")[0]) 174 | elif key == "Memory usage": 175 | memory_usage_in_bytes = int(value) 176 | 177 | elapsed_seconds_values.append(elapsed_seconds) 178 | memory_usage_in_bytes_values.append(memory_usage_in_bytes) 179 | 180 | elapsed_seconds_median = statistics.median(elapsed_seconds_values) 181 | memory_usage_in_bytes_median = statistics.median( 182 | memory_usage_in_bytes_values 183 | ) 184 | results.append( 185 | [ 186 | hash_table_name, 187 | hash_function_name, 188 | f"{elapsed_seconds_median:.2f}", 189 | format_readable_size(memory_usage_in_bytes_median), 190 | ] 191 | ) 192 | 193 | print( 194 | f"File: {file}\nKey type: {key_type_name}\nKeys size: {keys_size}\nUnique keys size: {hash_table_max_keys_size}" 195 | ) 196 | 197 | table = PrettyTable() 198 | table.field_names = [ 199 | "Hash Table", 200 | "Hash Function", 201 | "Elapsed (sec)", 202 | "Memory Usage", 203 | ] 204 | table.align["Hash Table"] = "l" 205 | table.align["Hash Function"] = "l" 206 | table.align["Memory Usage"] = "r" 207 | table.add_rows(results) 208 | 209 | print(table) 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/HashMap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "HashTable.h" 4 | #include "HashTableAllocator.h" 5 | #include "Hash.h" 6 | 7 | // #include 8 | // #include 9 | // #include 10 | // #include 11 | 12 | 13 | /** NOTE HashMap could only be used for memmoveable (position independent) types. 14 | * Example: std::string is not position independent in libstdc++ with C++11 ABI or in libc++. 15 | * Also, key in hash table must be of type, that zero bytes is compared equals to zero key. 16 | * 17 | * Please keep in sync with PackedHashMap.h 18 | */ 19 | 20 | namespace DB 21 | { 22 | namespace ErrorCodes 23 | { 24 | extern const int LOGICAL_ERROR; 25 | } 26 | } 27 | 28 | struct NoInitTag 29 | { 30 | }; 31 | 32 | /// A pair that does not initialize the elements, if not needed. 33 | template 34 | struct PairNoInit 35 | { 36 | First first; 37 | Second second; 38 | 39 | PairNoInit() {} /// NOLINT 40 | 41 | template 42 | PairNoInit(FirstValue && first_, NoInitTag) 43 | : first(std::forward(first_)) 44 | { 45 | } 46 | 47 | template 48 | PairNoInit(FirstValue && first_, SecondValue && second_) 49 | : first(std::forward(first_)) 50 | , second(std::forward(second_)) 51 | { 52 | } 53 | }; 54 | 55 | template 56 | PairNoInit, std::decay_t> makePairNoInit(First && first, Second && second) 57 | { 58 | return PairNoInit, std::decay_t>(std::forward(first), std::forward(second)); 59 | } 60 | 61 | 62 | template > 63 | struct HashMapCell 64 | { 65 | using Mapped = TMapped; 66 | using State = TState; 67 | 68 | using value_type = Pair; 69 | using mapped_type = Mapped; 70 | using key_type = Key; 71 | 72 | value_type value; 73 | 74 | HashMapCell() = default; 75 | HashMapCell(const Key & key_, const State &) : value(key_, NoInitTag()) {} 76 | HashMapCell(const value_type & value_, const State &) : value(value_) {} 77 | 78 | /// Get the key (externally). 79 | const Key & getKey() const { return value.first; } 80 | Mapped & getMapped() { return value.second; } 81 | const Mapped & getMapped() const { return value.second; } 82 | const value_type & getValue() const { return value; } 83 | 84 | /// Get the key (internally). 85 | static const Key & getKey(const value_type & value) { return value.first; } 86 | 87 | bool keyEquals(const Key & key_) const { return bitEquals(value.first, key_); } 88 | bool keyEquals(const Key & key_, size_t /*hash_*/) const { return bitEquals(value.first, key_); } 89 | bool keyEquals(const Key & key_, size_t /*hash_*/, const State & /*state*/) const { return bitEquals(value.first, key_); } 90 | 91 | void setHash(size_t /*hash_value*/) {} 92 | size_t getHash(const Hash & hash) const { return hash(value.first); } 93 | 94 | bool isZero(const State & state) const { return isZero(value.first, state); } 95 | static bool isZero(const Key & key, const State & /*state*/) { return ZeroTraits::check(key); } 96 | 97 | /// Set the key value to zero. 98 | void setZero() { ZeroTraits::set(value.first); } 99 | 100 | /// Do I need to store the zero key separately (that is, can a zero key be inserted into the hash table). 101 | static constexpr bool need_zero_value_storage = true; 102 | 103 | void setMapped(const value_type & value_) { value.second = value_.second; } 104 | 105 | /// Serialization, in binary and text form. 106 | // void write(DB::WriteBuffer & wb) const 107 | // { 108 | // DB::writeBinary(value.first, wb); 109 | // DB::writeBinary(value.second, wb); 110 | // } 111 | 112 | // void writeText(DB::WriteBuffer & wb) const 113 | // { 114 | // DB::writeDoubleQuoted(value.first, wb); 115 | // DB::writeChar(',', wb); 116 | // DB::writeDoubleQuoted(value.second, wb); 117 | // } 118 | 119 | /// Deserialization, in binary and text form. 120 | // void read(DB::ReadBuffer & rb) 121 | // { 122 | // DB::readBinary(value.first, rb); 123 | // DB::readBinary(value.second, rb); 124 | // } 125 | 126 | // void readText(DB::ReadBuffer & rb) 127 | // { 128 | // DB::readDoubleQuoted(value.first, rb); 129 | // DB::assertChar(',', rb); 130 | // DB::readDoubleQuoted(value.second, rb); 131 | // } 132 | 133 | static bool constexpr need_to_notify_cell_during_move = false; 134 | 135 | static void move(HashMapCell * /* old_location */, HashMapCell * /* new_location */) {} 136 | 137 | template 138 | auto & get() & { 139 | if constexpr (I == 0) return value.first; 140 | else if constexpr (I == 1) return value.second; 141 | } 142 | 143 | template 144 | auto const & get() const & { 145 | if constexpr (I == 0) return value.first; 146 | else if constexpr (I == 1) return value.second; 147 | } 148 | 149 | template 150 | auto && get() && { 151 | if constexpr (I == 0) return std::move(value.first); 152 | else if constexpr (I == 1) return std::move(value.second); 153 | } 154 | 155 | }; 156 | 157 | namespace std 158 | { 159 | 160 | template 161 | struct tuple_size> : std::integral_constant { }; 162 | 163 | template 164 | struct tuple_element<0, HashMapCell> { using type = Key; }; 165 | 166 | template 167 | struct tuple_element<1, HashMapCell> { using type = TMapped; }; 168 | } 169 | 170 | template 171 | struct HashMapCellWithSavedHash : public HashMapCell 172 | { 173 | using Base = HashMapCell; 174 | 175 | size_t saved_hash; 176 | 177 | using Base::Base; 178 | 179 | bool keyEquals(const Key & key_) const { return bitEquals(this->value.first, key_); } 180 | bool keyEquals(const Key & key_, size_t hash_) const { return saved_hash == hash_ && bitEquals(this->value.first, key_); } 181 | bool keyEquals(const Key & key_, size_t hash_, const typename Base::State &) const { return keyEquals(key_, hash_); } 182 | 183 | void setHash(size_t hash_value) { saved_hash = hash_value; } 184 | size_t getHash(const Hash & /*hash_function*/) const { return saved_hash; } 185 | }; 186 | 187 | template < 188 | typename Key, 189 | typename Cell, 190 | typename Hash = DefaultHash, 191 | typename Grower = HashTableGrowerWithPrecalculation<>, 192 | typename Allocator = HashTableAllocator> 193 | class HashMapTable : public HashTable 194 | { 195 | public: 196 | using Self = HashMapTable; 197 | using Base = HashTable; 198 | using LookupResult = typename Base::LookupResult; 199 | using Iterator = typename Base::iterator; 200 | 201 | using Base::Base; 202 | using Base::prefetch; 203 | 204 | /// Merge every cell's value of current map into the destination map via emplace. 205 | /// Func should have signature void(Mapped & dst, Mapped & src, bool emplaced). 206 | /// Each filled cell in current map will invoke func once. If that map doesn't 207 | /// have a key equals to the given cell, a new cell gets emplaced into that map, 208 | /// and func is invoked with the third argument emplaced set to true. Otherwise 209 | /// emplaced is set to false. 210 | // template 211 | // void ALWAYS_INLINE mergeToViaEmplace(Self & that, Func && func) 212 | // { 213 | // DB::PrefetchingHelper prefetching; 214 | // size_t prefetch_look_ahead = prefetching.getInitialLookAheadValue(); 215 | 216 | // size_t i = 0; 217 | // auto prefetch_it = advanceIterator(this->begin(), prefetch_look_ahead); 218 | 219 | // for (auto it = this->begin(), end = this->end(); it != end; ++it, ++i) 220 | // { 221 | // if constexpr (prefetch) 222 | // { 223 | // if (i == prefetching.iterationsToMeasure()) 224 | // { 225 | // prefetch_look_ahead = prefetching.calcPrefetchLookAhead(); 226 | // prefetch_it = advanceIterator(prefetch_it, prefetch_look_ahead - prefetching.getInitialLookAheadValue()); 227 | // } 228 | 229 | // if (prefetch_it != end) 230 | // { 231 | // that.prefetchByHash(prefetch_it.getHash()); 232 | // ++prefetch_it; 233 | // } 234 | // } 235 | 236 | // typename Self::LookupResult res_it; 237 | // bool inserted; 238 | // that.emplace(Cell::getKey(it->getValue()), res_it, inserted, it.getHash()); 239 | // func(res_it->getMapped(), it->getMapped(), inserted); 240 | // } 241 | // } 242 | 243 | /// Merge every cell's value of current map into the destination map via find. 244 | /// Func should have signature void(Mapped & dst, Mapped & src, bool exist). 245 | /// Each filled cell in current map will invoke func once. If that map doesn't 246 | /// have a key equals to the given cell, func is invoked with the third argument 247 | /// exist set to false. Otherwise exist is set to true. 248 | template 249 | void ALWAYS_INLINE mergeToViaFind(Self & that, Func && func) 250 | { 251 | for (auto it = this->begin(), end = this->end(); it != end; ++it) 252 | { 253 | auto res_it = that.find(Cell::getKey(it->getValue()), it.getHash()); 254 | if (!res_it) 255 | func(it->getMapped(), it->getMapped(), false); 256 | else 257 | func(res_it->getMapped(), it->getMapped(), true); 258 | } 259 | } 260 | 261 | /// Call func(const Key &, Mapped &) for each hash map element. 262 | template 263 | void forEachValue(Func && func) 264 | { 265 | for (auto & v : *this) 266 | func(v.getKey(), v.getMapped()); 267 | } 268 | 269 | /// Call func(Mapped &) for each hash map element. 270 | template 271 | void forEachMapped(Func && func) 272 | { 273 | for (auto & v : *this) 274 | func(v.getMapped()); 275 | } 276 | 277 | typename Cell::Mapped & ALWAYS_INLINE operator[](const Key & x) 278 | { 279 | LookupResult it; 280 | bool inserted; 281 | this->emplace(x, it, inserted); 282 | 283 | /** It may seem that initialization is not necessary for POD-types (or __has_trivial_constructor), 284 | * since the hash table memory is initially initialized with zeros. 285 | * But, in fact, an empty cell may not be initialized with zeros in the following cases: 286 | * - ZeroValueStorage (it only zeros the key); 287 | * - after resizing and moving a part of the cells to the new half of the hash table, the old cells also have only the key to zero. 288 | * 289 | * On performance, there is almost always no difference, due to the fact that it->second is usually assigned immediately 290 | * after calling `operator[]`, and since `operator[]` is inlined, the compiler removes unnecessary initialization. 291 | * 292 | * Sometimes due to initialization, the performance even grows. This occurs in code like `++map[key]`. 293 | * When we do the initialization, for new cells, it's enough to make `store 1` right away. 294 | * And if we did not initialize, then even though there was zero in the cell, 295 | * the compiler can not guess about this, and generates the `load`, `increment`, `store` code. 296 | */ 297 | if (inserted) 298 | new (&it->getMapped()) typename Cell::Mapped(); 299 | 300 | return it->getMapped(); 301 | } 302 | 303 | const typename Cell::Mapped & ALWAYS_INLINE at(const Key & x) const 304 | { 305 | if (auto it = this->find(x); it != this->end()) 306 | return it->getMapped(); 307 | 308 | throw std::logic_error("Cannot find element in HashMap::at method"); 309 | } 310 | 311 | private: 312 | Iterator advanceIterator(Iterator it, size_t n) 313 | { 314 | size_t i = 0; 315 | while (i < n && it != this->end()) 316 | { 317 | ++i; 318 | ++it; 319 | } 320 | return it; 321 | } 322 | }; 323 | 324 | namespace std 325 | { 326 | 327 | template 328 | struct tuple_size> : std::integral_constant { }; 329 | 330 | template 331 | struct tuple_element<0, HashMapCellWithSavedHash> { using type = Key; }; 332 | 333 | template 334 | struct tuple_element<1, HashMapCellWithSavedHash> { using type = TMapped; }; 335 | } 336 | 337 | 338 | template < 339 | typename Key, 340 | typename Mapped, 341 | typename Hash = DefaultHash, 342 | typename Grower = HashTableGrowerWithPrecalculation<>, 343 | typename Allocator = HashTableAllocator> 344 | using HashMap = HashMapTable, Hash, Grower, Allocator>; 345 | 346 | 347 | template < 348 | typename Key, 349 | typename Mapped, 350 | typename Hash = DefaultHash, 351 | typename Grower = HashTableGrowerWithPrecalculation<>, 352 | typename Allocator = HashTableAllocator> 353 | using HashMapWithSavedHash = HashMapTable, Hash, Grower, Allocator>; 354 | 355 | template 357 | using HashMapWithStackMemory = HashMapTable< 358 | Key, 359 | HashMapCellWithSavedHash, 360 | Hash, 361 | HashTableGrower, 362 | HashTableAllocatorWithStackMemory< 363 | (1ULL << initial_size_degree) 364 | * sizeof(HashMapCellWithSavedHash)>>; 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hash tables aggregation benchmark 2 | 3 | ## Motivation 4 | 5 | This benchmark is created to compare the performance of different hash tables with different hash functions in in-memory aggregation scenario. 6 | 7 | Benchmark is based on real anonymized web analytics data from [Yandex.Metrica dataset](https://clickhouse.com/docs/en/getting-started/example-datasets/metrica). 8 | 9 | Benchmark computes mapping for each unique key to count for columns from the dataset, similar to such SQL query `SELECT column, count(column) FROM hits GROUP BY column`. 10 | 11 | Because each column in the benchmark has different cardinality and distribution, it is possible to check how different hash tables work for in-memory aggregation on real-world data. 12 | 13 | There are two different scenarios: 14 | 15 | 1. When hash table fits in CPU caches. Performance of hash table operations depends on arithmetic operations like hash function calculation, computing slot location, elements comparisons, and other operations that are required for specific hash table memory layout. 16 | 2. When hash table does not fit in CPU caches. For such a scenario, number of random memory accesses per operation is the most important factor for hash table performance. 17 | 18 | Here are some general recommendations that can be applied to all hash table implementations: 19 | 20 | 1. You should avoid complex logic on a hot path of your hash table operations. If there are a lot of instructions on the hot path, hash table will work slow when all data fits in CPU caches. 21 | 2. You should not store a lot of additional metadata in your hash table because your hash table will stop fitting into CPU caches quickly. 22 | 3. You should decrease the number of random memory accesses in your hash table operations because otherwise, it will significantly slow down your hash table implementation after hash table does not fit in CPU caches. Ideally, you should have one memory access for each operation. This also implies that you cannot make complex layouts (2 hash tables, keys and values separation, complex metadata) because usually, this will require additional memory accesses during operations. 23 | 24 | Additionally, for the aggregation scenario, you need to consider not only lookup/insert latency but also the memory size of hash table. If during aggregation hash table does not fit in RAM memory, the aggregation algorithm should switch implementation from in-memory aggregation to external memory aggregation, but this will work significantly slower. 25 | 26 | For more information, take a look at my blog post about [hash tables](https://maksimkita.com/blog/hash_tables.html). 27 | 28 | ## Examples 29 | 30 | Benchmark itself is a `hash_table_aggregation_benchmark` binary that takes `hash_table`, `hash_function`, and `file` in ClickHouse 31 | [RowBinaryWithNamesAndTypes](https://clickhouse.com/docs/en/interfaces/formats#rowbinarywithnamesandtypes) format and runs in-memory aggregation. 32 | 33 | ``` 34 | hash_table_aggregation_benchmark absl_hash_map absl_hash data/WatchID.bin 35 | 36 | Hash table type: absl_hash_map 37 | Hash function type: absl_hash 38 | Key type name: Int64 39 | File name: data/WatchID.bin 40 | Keys size: 99997497 41 | Hash table: absl::flat_hash_map 42 | Hash function: absl::Hash 43 | Hash table size: 99997493 44 | Elapsed: 10.7699 (9284909 elem/sec.) 45 | Memory usage: 2285594168 46 | ``` 47 | 48 | `benchmark.py` is just a wrapper around `hash_table_aggregation_benchmark` that provides the ability to specify multiple hash tables, hash functions, and files 49 | and run `hash_table_aggregation_benchmark` for each combination. 50 | 51 | Run benchmark to test Abseil Hash Map using Abseil hash function for `WatchID` column: 52 | 53 | ``` 54 | ./benchmark.py --hash-tables="absl_hash_map" --hash-functions="absl_hash" --files="data/WatchID.bin" 55 | 56 | File: data/WatchID.bin 57 | Key type: Int64 58 | Keys size: 99997497 59 | Unique keys size: 99997493 60 | +---------------------+---------------+---------------+--------------+ 61 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 62 | +---------------------+---------------+---------------+--------------+ 63 | | absl::flat_hash_map | absl::Hash | 10.77 | 2.13 GiB | 64 | +---------------------+---------------+---------------+--------------+ 65 | ``` 66 | 67 | Run benchmark to test ClickHouse Hash Map using all available hash functions for `WatchID` column: 68 | 69 | ``` 70 | ./benchmark.py --hash-tables="ch_hash_map" --files="data/WatchID.bin" 71 | 72 | File: data/WatchID.bin 73 | Key type: Int64 74 | Keys size: 99997497 75 | Unique keys size: 99997493 76 | +--------------------+-----------------+---------------+--------------+ 77 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 78 | +--------------------+-----------------+---------------+--------------+ 79 | | ClickHouse HashMap | std::hash | 6.14 | 4.00 GiB | 80 | | ClickHouse HashMap | ClickHouse hash | 6.98 | 4.00 GiB | 81 | | ClickHouse HashMap | absl::Hash | 6.55 | 4.00 GiB | 82 | +--------------------+-----------------+---------------+--------------+ 83 | ``` 84 | 85 | Run benchmark to test ClickHouse Hash Map and Abseil Hash Map using Abseil hash function and standard hash function for `WatchID` and `UserID` columns: 86 | 87 | ``` 88 | ./benchmark.py --hash-tables="ch_hash_map, absl_hash_map" --hash-functions="absl_hash, std_hash" --files="data/WatchID.bin, data/UserID.bin" 89 | 90 | File: data/WatchID.bin 91 | Key type: Int64 92 | Keys size: 99997497 93 | Unique keys size: 99997493 94 | +---------------------+---------------+---------------+--------------+ 95 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 96 | +---------------------+---------------+---------------+--------------+ 97 | | ClickHouse HashMap | absl::Hash | 6.56 | 4.00 GiB | 98 | | ClickHouse HashMap | std::hash | 6.19 | 4.00 GiB | 99 | | absl::flat_hash_map | absl::Hash | 10.53 | 2.13 GiB | 100 | | absl::flat_hash_map | std::hash | 9.31 | 2.13 GiB | 101 | +---------------------+---------------+---------------+--------------+ 102 | 103 | File: data/UserID.bin 104 | Key type: Int64 105 | Keys size: 99997497 106 | Unique keys size: 17630976 107 | +---------------------+---------------+---------------+--------------+ 108 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 109 | +---------------------+---------------+---------------+--------------+ 110 | | ClickHouse HashMap | absl::Hash | 1.96 | 1.00 GiB | 111 | | ClickHouse HashMap | std::hash | 1.86 | 1.00 GiB | 112 | | absl::flat_hash_map | absl::Hash | 2.82 | 547.74 MiB | 113 | | absl::flat_hash_map | std::hash | 2.71 | 547.62 MiB | 114 | +---------------------+---------------+---------------+--------------+ 115 | ``` 116 | 117 | You can extract columns from other datasets or generate columns using `clickhouse-local` (it is downloaded as part of the benchmark data load, or you can download it manually). 118 | For example, you can generate a column with 150 million random numbers and run a benchmark to check ClickHouse Hash Map and Abseil Hash Map against this column: 119 | 120 | ``` 121 | ./clickhouse local --multiquery "INSERT INTO TABLE FUNCTION file('RandomNumbers.bin', RowBinaryWithNamesAndTypes) SELECT rand64() FROM numbers_mt(150000000);" 122 | 123 | File: RandomNumbers.bin 124 | Key type: UInt64 125 | Keys size: 150000000 126 | Unique keys size: 150000000 127 | +---------------------+-----------------+---------------+--------------+ 128 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 129 | +---------------------+-----------------+---------------+--------------+ 130 | | ClickHouse HashMap | std::hash | 11.30 | 8.00 GiB | 131 | | ClickHouse HashMap | ClickHouse hash | 13.08 | 8.00 GiB | 132 | | ClickHouse HashMap | absl::Hash | 12.01 | 8.00 GiB | 133 | | absl::flat_hash_map | std::hash | 15.72 | 4.25 GiB | 134 | | absl::flat_hash_map | ClickHouse hash | 17.98 | 4.25 GiB | 135 | | absl::flat_hash_map | absl::Hash | 17.58 | 4.25 GiB | 136 | +---------------------+-----------------+---------------+--------------+ 137 | ``` 138 | 139 | ## Prerequisites 140 | 141 | 1. git 142 | 2. python3 with pip installed 143 | 3. cmake with minimum version 3.20 144 | 4. clang-15 or higher 145 | 5. wget 146 | 6. curl 147 | 148 | For Ubuntu Linux with 22.04 LTS these prerequisites can be downloaded using the following command: 149 | ``` 150 | sudo apt install git cmake clang-15 python3 python3-pip wget curl 151 | ``` 152 | 153 | ## Build instructions 154 | 155 | Clone repository with benchmark and checkout submodules 156 | 157 | ``` 158 | git clone git@github.com:kitaisreal/hash-table-aggregation-benchmark.git 159 | cd hash-table-aggregation-benchmark 160 | git submodule update --init --recursive 161 | ``` 162 | 163 | Download benchmark data (a new `data` folder will be created). All benchmark data takes around 20 GB. 164 | 165 | ``` 166 | ./load_data.sh 167 | ``` 168 | 169 | Download python dependencies from `requirements.txt`: 170 | 171 | ``` 172 | python3 -m pip install -r requirements.txt 173 | ``` 174 | 175 | Build benchmark and add the folder with result binary to PATH: 176 | 177 | ``` 178 | cd build 179 | cmake .. -DCMAKE_CXX_COMPILER=/usr/bin/clang++-15 -DCMAKE_C_COMPILER=/usr/bin/clang-15 -DCMAKE_BUILD_TYPE=RelWithDebInfo 180 | make -j32 181 | export PATH=`pwd`/src:$PATH 182 | ``` 183 | 184 | Run benchmark with different `--hash-tables`, `--hash-functions`, and `--files` options. By default, all hash tables, hash functions 185 | and files from the benchmark are specified. 186 | 187 | ``` 188 | ./benchmark.py 189 | ``` 190 | 191 | ## Hash tables included 192 | 193 | - [x] ClickHouse Hash Map (https://github.com/ClickHouse/ClickHouse) 194 | - [x] Google Abseil Hash Map (https://github.com/abseil/abseil-cpp) 195 | - [x] Google Dense Hash Map (https://github.com/sparsehash/sparsehash-c11) 196 | - [x] Tsl Hopscotch Map (https://github.com/Tessil/hopscotch-map) 197 | - [ ] Tsl Robin Hood Map (https://github.com/Tessil/robin-map) 198 | - [x] Ankerl Unordered Dense Map (https://github.com/martinus/unordered_dense) 199 | - [x] Ska Flat Hash Map (https://github.com/skarupke/flat_hash_map) 200 | - [x] Ska Bytell Hash Map (https://github.com/skarupke/flat_hash_map) 201 | - [x] Standard Unordered Map (depends on standard library implementation) 202 | 203 | ## Hash functions included 204 | 205 | - [x] ClickHouse Hash (https://github.com/ClickHouse/ClickHouse) 206 | - [x] Abseil Hash (https://github.com/abseil/abseil-cpp) 207 | - [x] Standard Hash (depends on standard library implementation) 208 | 209 | ## Results 210 | 211 | All tests were run on `c6a.4xlarge` VM in AWS for X86-64 platform and on `m7g.4xlarge` in AWS for ARM platform with 128 GB gp2. 212 | 213 | Results for X86-64 on `c6a.4xlarge` instance for all hash tables with Abseil hash on `WatchID`, `UserID` and `RegionID` columns. 214 | For full results, see [Results.md](Results.md). 215 | 216 | WatchID file: 217 | 218 | ``` 219 | File: data/WatchID.bin 220 | Key type: Int64 221 | Keys size: 99997497 222 | Unique keys size: 99997493 223 | +------------------------------+-----------------+---------------+--------------+ 224 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 225 | +------------------------------+-----------------+---------------+--------------+ 226 | | ClickHouse HashMap | absl::Hash | 6.70 | 4.00 GiB | 227 | | absl::flat_hash_map | absl::Hash | 10.01 | 2.13 GiB | 228 | | google::dense_hash_map | absl::Hash | 9.88 | 4.00 GiB | 229 | | tsl::hopscotch_map | absl::Hash | 16.02 | 3.00 GiB | 230 | | ankerl::unordered_dense::map | absl::Hash | 12.93 | 2.49 GiB | 231 | | ska::flat_hash_map | absl::Hash | 10.95 | 6.00 GiB | 232 | | ska::bytell_hash_map | absl::Hash | 15.04 | 2.13 GiB | 233 | | std::unordered_map | absl::Hash | 58.03 | 5.23 GiB | 234 | +------------------------------+-----------------+---------------+--------------+ 235 | ``` 236 | 237 | UserID file: 238 | 239 | ``` 240 | File: data/UserID.bin 241 | Key type: Int64 242 | Keys size: 99997497 243 | Unique keys size: 17630976 244 | +------------------------------+-----------------+---------------+--------------+ 245 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 246 | +------------------------------+-----------------+---------------+--------------+ 247 | | ClickHouse HashMap | absl::Hash | 2.03 | 1.00 GiB | 248 | | absl::flat_hash_map | absl::Hash | 2.65 | 544.60 MiB | 249 | | google::dense_hash_map | absl::Hash | 2.70 | 1.00 GiB | 250 | | tsl::hopscotch_map | absl::Hash | 3.59 | 768.64 MiB | 251 | | ankerl::unordered_dense::map | absl::Hash | 3.17 | 525.43 MiB | 252 | | ska::flat_hash_map | absl::Hash | 2.64 | 1.50 GiB | 253 | | ska::bytell_hash_map | absl::Hash | 3.24 | 544.55 MiB | 254 | | std::unordered_map | absl::Hash | 8.81 | 995.00 MiB | 255 | +------------------------------+-----------------+---------------+--------------+ 256 | ``` 257 | 258 | RegionID file: 259 | 260 | ``` 261 | File: data/RegionID.bin 262 | Key type: Int32 263 | Keys size: 99997497 264 | Unique keys size: 9040 265 | +------------------------------+-----------------+---------------+--------------+ 266 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 267 | +------------------------------+-----------------+---------------+--------------+ 268 | | ClickHouse HashMap | absl::Hash | 0.17 | 1.15 MiB | 269 | | absl::flat_hash_map | absl::Hash | 0.32 | 720.00 KiB | 270 | | google::dense_hash_map | absl::Hash | 0.32 | 936.00 KiB | 271 | | tsl::hopscotch_map | absl::Hash | 0.34 | 876.00 KiB | 272 | | ankerl::unordered_dense::map | absl::Hash | 0.52 | 560.00 KiB | 273 | | ska::flat_hash_map | absl::Hash | 0.23 | 1.17 MiB | 274 | | ska::bytell_hash_map | absl::Hash | 0.28 | 700.00 KiB | 275 | | std::unordered_map | absl::Hash | 0.64 | 764.00 KiB | 276 | +------------------------------+-----------------+---------------+--------------+ 277 | ``` 278 | 279 | # How to add a new hash table or hash function 280 | 281 | Add a submodule with a hash table or hash function to the repository. 282 | 283 | Add a new hash table in `src/HashTables.h` and update `benchmark.py`. 284 | 285 | Add a new hash function in `src/HashFunctions.h` and update `benchmark.py`. 286 | 287 | # Contacts 288 | 289 | If you have any questions or suggestions, you can contact me at kitaetoya@gmail.com. 290 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/Allocator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include "types.h" 5 | #include 6 | #include 7 | 8 | template 9 | class Allocator; 10 | 11 | template 12 | class AllocatorWithStackMemory; 13 | 14 | #ifdef NDEBUG 15 | #define ALLOCATOR_ASLR 0 16 | #else 17 | #define ALLOCATOR_ASLR 1 18 | #endif 19 | 20 | // #include 21 | // #include 22 | 23 | #if !defined(OS_DARWIN) && !defined(OS_FREEBSD) 24 | #include 25 | #endif 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | // #include 32 | #if defined(THREAD_SANITIZER) || defined(MEMORY_SANITIZER) 33 | /// Thread and memory sanitizers do not intercept mremap. The usage of 34 | /// mremap will lead to false positives. 35 | #define DISABLE_MREMAP 1 36 | #endif 37 | 38 | /// mpremap.h 39 | 40 | #include 41 | #include 42 | 43 | #ifdef MREMAP_MAYMOVE 44 | #define HAS_MREMAP 1 45 | #else 46 | #define HAS_MREMAP 0 47 | #endif 48 | 49 | 50 | /// You can forcely disable mremap by defining DISABLE_MREMAP to 1 before including this file. 51 | #ifndef DISABLE_MREMAP 52 | #if HAS_MREMAP 53 | #define DISABLE_MREMAP 0 54 | #else 55 | #define DISABLE_MREMAP 1 56 | #endif 57 | #endif 58 | 59 | 60 | /// Implement mremap with mmap/memcpy/munmap. 61 | void * mremap_fallback( 62 | void * old_address, 63 | size_t old_size, 64 | size_t new_size, 65 | int flags, 66 | int mmap_prot, 67 | int mmap_flags, 68 | int mmap_fd, 69 | off_t mmap_offset); 70 | 71 | 72 | #if !HAS_MREMAP 73 | #define MREMAP_MAYMOVE 1 74 | 75 | inline void * mremap( 76 | void * old_address, 77 | size_t old_size, 78 | size_t new_size, 79 | int flags = 0, 80 | int mmap_prot = 0, 81 | int mmap_flags = 0, 82 | int mmap_fd = -1, 83 | off_t mmap_offset = 0) 84 | { 85 | return mremap_fallback(old_address, old_size, new_size, flags, mmap_prot, mmap_flags, mmap_fd, mmap_offset); 86 | } 87 | #endif 88 | 89 | 90 | inline void * clickhouse_mremap( 91 | void * old_address, 92 | size_t old_size, 93 | size_t new_size, 94 | int flags = 0, 95 | [[maybe_unused]] int mmap_prot = 0, 96 | [[maybe_unused]] int mmap_flags = 0, 97 | [[maybe_unused]] int mmap_fd = -1, 98 | [[maybe_unused]] off_t mmap_offset = 0) 99 | { 100 | #if DISABLE_MREMAP 101 | return mremap_fallback(old_address, old_size, new_size, flags, mmap_prot, mmap_flags, mmap_fd, mmap_offset); 102 | #else 103 | 104 | return mremap( 105 | old_address, 106 | old_size, 107 | new_size, 108 | flags 109 | #if !defined(MREMAP_FIXED) 110 | , 111 | mmap_prot, 112 | mmap_flags, 113 | mmap_fd, 114 | mmap_offset 115 | #endif 116 | ); 117 | #endif 118 | } 119 | 120 | 121 | /// getPageSize.h 122 | 123 | 124 | /// Get memory page size 125 | Int64 getPageSize(); 126 | 127 | 128 | // #include 129 | // #include 130 | // #include 131 | // #include 132 | 133 | // #include 134 | 135 | 136 | /// Required for older Darwin builds, that lack definition of MAP_ANONYMOUS 137 | #ifndef MAP_ANONYMOUS 138 | #define MAP_ANONYMOUS MAP_ANON 139 | #endif 140 | 141 | /** 142 | * Many modern allocators (for example, tcmalloc) do not do a mremap for 143 | * realloc, even in case of large enough chunks of memory. Although this allows 144 | * you to increase performance and reduce memory consumption during realloc. 145 | * To fix this, we do mremap manually if the chunk of memory is large enough. 146 | * The threshold (64 MB) is chosen quite large, since changing the address 147 | * space is very slow, especially in the case of a large number of threads. We 148 | * expect that the set of operations mmap/something to do/mremap can only be 149 | * performed about 1000 times per second. 150 | * 151 | * P.S. This is also required, because tcmalloc can not allocate a chunk of 152 | * memory greater than 16 GB. 153 | * 154 | * P.P.S. Note that MMAP_THRESHOLD symbol is intentionally made weak. It allows 155 | * to override it during linkage when using ClickHouse as a library in 156 | * third-party applications which may already use own allocator doing mmaps 157 | * in the implementation of alloc/realloc. 158 | */ 159 | extern const size_t MMAP_THRESHOLD; 160 | 161 | static constexpr size_t MALLOC_MIN_ALIGNMENT = 8; 162 | 163 | // namespace CurrentMetrics 164 | // { 165 | // extern const Metric MMappedAllocs; 166 | // extern const Metric MMappedAllocBytes; 167 | // } 168 | 169 | // namespace DB 170 | // { 171 | // namespace ErrorCodes 172 | // { 173 | // extern const int BAD_ARGUMENTS; 174 | // extern const int CANNOT_ALLOCATE_MEMORY; 175 | // extern const int CANNOT_MUNMAP; 176 | // extern const int CANNOT_MREMAP; 177 | // extern const int LOGICAL_ERROR; 178 | // } 179 | // } 180 | 181 | /** Responsible for allocating / freeing memory. Used, for example, in PODArray, Arena. 182 | * Also used in hash tables. 183 | * The interface is different from std::allocator 184 | * - the presence of the method realloc, which for large chunks of memory uses mremap; 185 | * - passing the size into the `free` method; 186 | * - by the presence of the `alignment` argument; 187 | * - the possibility of zeroing memory (used in hash tables); 188 | * - random hint address for mmap 189 | * - mmap_threshold for using mmap less or more 190 | */ 191 | template 192 | class Allocator 193 | { 194 | public: 195 | /// Allocate memory range. 196 | void * alloc(size_t size, size_t alignment = 0) 197 | { 198 | checkSize(size); 199 | // CurrentMemoryTracker::alloc(size); 200 | return allocNoTrack(size, alignment); 201 | } 202 | 203 | /// Free memory range. 204 | void free(void * buf, size_t size) 205 | { 206 | try 207 | { 208 | checkSize(size); 209 | freeNoTrack(buf, size); 210 | // CurrentMemoryTracker::free(size); 211 | } 212 | catch (...) 213 | { 214 | // DB::tryLogCurrentException("Allocator::free"); 215 | throw; 216 | } 217 | } 218 | 219 | /** Enlarge memory range. 220 | * Data from old range is moved to the beginning of new range. 221 | * Address of memory range could change. 222 | */ 223 | void * realloc(void * buf, size_t old_size, size_t new_size, size_t alignment = 0) 224 | { 225 | checkSize(new_size); 226 | 227 | if (old_size == new_size) 228 | { 229 | /// nothing to do. 230 | /// BTW, it's not possible to change alignment while doing realloc. 231 | } 232 | else if (old_size < MMAP_THRESHOLD && new_size < MMAP_THRESHOLD 233 | && alignment <= MALLOC_MIN_ALIGNMENT) 234 | { 235 | /// Resize malloc'd memory region with no special alignment requirement. 236 | // CurrentMemoryTracker::realloc(old_size, new_size); 237 | 238 | void * new_buf = ::realloc(buf, new_size); 239 | if (nullptr == new_buf) 240 | throw std::runtime_error("Allocator: Cannot realloc"); 241 | // DB::throwFromErrno(fmt::format("Allocator: Cannot realloc from {} to {}.", ReadableSize(old_size), ReadableSize(new_size)), DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY); 242 | 243 | buf = new_buf; 244 | if constexpr (clear_memory) 245 | if (new_size > old_size) 246 | memset(reinterpret_cast(buf) + old_size, 0, new_size - old_size); 247 | } 248 | else if (old_size >= MMAP_THRESHOLD && new_size >= MMAP_THRESHOLD) 249 | { 250 | /// Resize mmap'd memory region. 251 | // CurrentMemoryTracker::realloc(old_size, new_size); 252 | 253 | // On apple and freebsd self-implemented mremap used (common/mremap.h) 254 | buf = clickhouse_mremap(buf, old_size, new_size, MREMAP_MAYMOVE, 255 | PROT_READ | PROT_WRITE, mmap_flags, -1, 0); 256 | if (MAP_FAILED == buf) 257 | throw std::runtime_error("Allocator: Cannot mremap memory chunk " + std::string(strerror(errno))); 258 | // DB::throwFromErrno(fmt::format("Allocator: Cannot mremap memory chunk from {} to {}.", 259 | // ReadableSize(old_size), ReadableSize(new_size)), DB::ErrorCodes::CANNOT_MREMAP); 260 | 261 | /// No need for zero-fill, because mmap guarantees it. 262 | } 263 | else if (new_size < MMAP_THRESHOLD) 264 | { 265 | /// Small allocs that requires a copy. Assume there's enough memory in system. Call CurrentMemoryTracker once. 266 | // CurrentMemoryTracker::realloc(old_size, new_size); 267 | 268 | void * new_buf = allocNoTrack(new_size, alignment); 269 | memcpy(new_buf, buf, std::min(old_size, new_size)); 270 | freeNoTrack(buf, old_size); 271 | buf = new_buf; 272 | } 273 | else 274 | { 275 | /// Big allocs that requires a copy. MemoryTracker is called inside 'alloc', 'free' methods. 276 | 277 | void * new_buf = alloc(new_size, alignment); 278 | memcpy(new_buf, buf, std::min(old_size, new_size)); 279 | free(buf, old_size); 280 | buf = new_buf; 281 | } 282 | 283 | return buf; 284 | } 285 | 286 | protected: 287 | static constexpr size_t getStackThreshold() 288 | { 289 | return 0; 290 | } 291 | 292 | static constexpr bool clear_memory = clear_memory_; 293 | 294 | // Freshly mmapped pages are copy-on-write references to a global zero page. 295 | // On the first write, a page fault occurs, and an actual writable page is 296 | // allocated. If we are going to use this memory soon, such as when resizing 297 | // hash tables, it makes sense to pre-fault the pages by passing 298 | // MAP_POPULATE to mmap(). This takes some time, but should be faster 299 | // overall than having a hot loop interrupted by page faults. 300 | // It is only supported on Linux. 301 | static constexpr int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS 302 | #if defined(OS_LINUX) 303 | | (mmap_populate ? MAP_POPULATE : 0) 304 | #endif 305 | ; 306 | 307 | private: 308 | void * allocNoTrack(size_t size, size_t alignment) 309 | { 310 | void * buf; 311 | size_t mmap_min_alignment = ::getPageSize(); 312 | 313 | if (size >= MMAP_THRESHOLD) 314 | { 315 | if (alignment > mmap_min_alignment) 316 | throw std::runtime_error("Too large alignment, more than page size when allocating"); 317 | // throw DB::Exception(DB::ErrorCodes::BAD_ARGUMENTS, 318 | // "Too large alignment {}: more than page size when allocating {}.", 319 | // ReadableSize(alignment), ReadableSize(size)); 320 | 321 | buf = mmap(getMmapHint(), size, PROT_READ | PROT_WRITE, 322 | mmap_flags, -1, 0); 323 | if (MAP_FAILED == buf) 324 | throw std::runtime_error("Allocator: Cannot mmap"); 325 | // DB::throwFromErrno(fmt::format("Allocator: Cannot mmap {}.", ReadableSize(size)), DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY); 326 | /// No need for zero-fill, because mmap guarantees it. 327 | 328 | // CurrentMetrics::add(CurrentMetrics::MMappedAllocs); 329 | // CurrentMetrics::add(CurrentMetrics::MMappedAllocBytes, size); 330 | } 331 | else 332 | { 333 | if (alignment <= MALLOC_MIN_ALIGNMENT) 334 | { 335 | if constexpr (clear_memory) 336 | buf = ::calloc(size, 1); 337 | else 338 | buf = ::malloc(size); 339 | 340 | if (nullptr == buf) 341 | throw std::runtime_error("Allocator: Cannot malloc"); 342 | // DB::throwFromErrno(fmt::format("Allocator: Cannot malloc {}.", ReadableSize(size)), DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY); 343 | } 344 | else 345 | { 346 | buf = nullptr; 347 | int res = posix_memalign(&buf, alignment, size); 348 | 349 | if (0 != res) 350 | throw std::runtime_error("Cannot allocate memory (posix_memalign)"); 351 | // DB::throwFromErrno(fmt::format("Cannot allocate memory (posix_memalign) {}.", ReadableSize(size)), 352 | // DB::ErrorCodes::CANNOT_ALLOCATE_MEMORY, res); 353 | 354 | if constexpr (clear_memory) 355 | memset(buf, 0, size); 356 | } 357 | } 358 | return buf; 359 | } 360 | 361 | void freeNoTrack(void * buf, size_t size) 362 | { 363 | if (size >= MMAP_THRESHOLD) 364 | { 365 | if (0 != munmap(buf, size)) 366 | throw std::runtime_error("Allocator: Cannot munmap"); 367 | // DB::throwFromErrno(fmt::format("Allocator: Cannot munmap {}.", ReadableSize(size)), DB::ErrorCodes::CANNOT_MUNMAP); 368 | 369 | // CurrentMetrics::sub(CurrentMetrics::MMappedAllocs); 370 | // CurrentMetrics::sub(CurrentMetrics::MMappedAllocBytes, size); 371 | } 372 | else 373 | { 374 | ::free(buf); 375 | } 376 | } 377 | 378 | void checkSize(size_t) 379 | { 380 | /// More obvious exception in case of possible overflow (instead of just "Cannot mmap"). 381 | // if (size >= 0x8000000000000000ULL) 382 | // throw DB::Exception(DB::ErrorCodes::LOGICAL_ERROR, "Too large size ({}) passed to allocator. It indicates an error.", size); 383 | } 384 | 385 | #ifndef NDEBUG 386 | /// In debug builds, request mmap() at random addresses (a kind of ASLR), to 387 | /// reproduce more memory stomping bugs. Note that Linux doesn't do it by 388 | /// default. This may lead to worse TLB performance. 389 | void * getMmapHint() 390 | { 391 | return reinterpret_cast(std::uniform_int_distribution(0x100000000000UL, 0x700000000000UL)(thread_local_rng)); 392 | } 393 | #else 394 | void * getMmapHint() 395 | { 396 | return nullptr; 397 | } 398 | #endif 399 | }; 400 | 401 | /** Allocator with optimization to place small memory ranges in automatic memory. 402 | */ 403 | template 404 | class AllocatorWithStackMemory : private Base 405 | { 406 | private: 407 | alignas(Alignment) char stack_memory[_initial_bytes]; 408 | 409 | public: 410 | static constexpr size_t initial_bytes = _initial_bytes; 411 | 412 | /// Do not use boost::noncopyable to avoid the warning about direct base 413 | /// being inaccessible due to ambiguity, when derived classes are also 414 | /// noncopiable (-Winaccessible-base). 415 | AllocatorWithStackMemory(const AllocatorWithStackMemory&) = delete; 416 | AllocatorWithStackMemory & operator = (const AllocatorWithStackMemory&) = delete; 417 | AllocatorWithStackMemory() = default; 418 | ~AllocatorWithStackMemory() = default; 419 | 420 | void * alloc(size_t size) 421 | { 422 | if (size <= initial_bytes) 423 | { 424 | if constexpr (Base::clear_memory) 425 | memset(stack_memory, 0, initial_bytes); 426 | return stack_memory; 427 | } 428 | 429 | return Base::alloc(size, Alignment); 430 | } 431 | 432 | void free(void * buf, size_t size) 433 | { 434 | if (size > initial_bytes) 435 | Base::free(buf, size); 436 | } 437 | 438 | void * realloc(void * buf, size_t old_size, size_t new_size) 439 | { 440 | /// Was in stack_memory, will remain there. 441 | if (new_size <= initial_bytes) 442 | return buf; 443 | 444 | /// Already was big enough to not fit in stack_memory. 445 | if (old_size > initial_bytes) 446 | return Base::realloc(buf, old_size, new_size, Alignment); 447 | 448 | /// Was in stack memory, but now will not fit there. 449 | void * new_buf = Base::alloc(new_size, Alignment); 450 | memcpy(new_buf, buf, old_size); 451 | return new_buf; 452 | } 453 | 454 | protected: 455 | static constexpr size_t getStackThreshold() 456 | { 457 | return initial_bytes; 458 | } 459 | }; 460 | 461 | // A constant that gives the number of initially available bytes in 462 | // the allocator. Used to check that this number is in sync with the 463 | // initial size of array or hash table that uses the allocator. 464 | template 465 | constexpr size_t allocatorInitialBytes = 0; 466 | 467 | template 468 | constexpr size_t allocatorInitialBytes> = initial_bytes; 470 | 471 | /// Prevent implicit template instantiation of Allocator 472 | 473 | extern template class Allocator; 474 | extern template class Allocator; 475 | extern template class Allocator; 476 | extern template class Allocator; 477 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/Hash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // #include 4 | // #include 5 | 6 | #include "types.h" 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | template 14 | inline T unalignedLoad(const void * address) 15 | { 16 | T res {}; 17 | memcpy(&res, address, sizeof(res)); 18 | return res; 19 | } 20 | 21 | /// We've had troubles before with wrong store size due to integral promotions 22 | /// (e.g., unalignedStore(dest, uint16_t + uint16_t) stores an uint32_t). 23 | /// To prevent this, make the caller specify the stored type explicitly. 24 | /// To disable deduction of T, wrap the argument type with std::enable_if. 25 | template 26 | inline void unalignedStore(void * address, 27 | const typename std::enable_if::type & src) 28 | { 29 | static_assert(std::is_trivially_copyable_v); 30 | memcpy(address, &src, sizeof(src)); 31 | } 32 | 33 | 34 | inline void reverseMemcpy(void * dst, const void * src, size_t size) 35 | { 36 | uint8_t * uint_dst = reinterpret_cast(dst); 37 | const uint8_t * uint_src = reinterpret_cast(src); 38 | 39 | uint_dst += size; 40 | while (size) 41 | { 42 | --uint_dst; 43 | *uint_dst = *uint_src; 44 | ++uint_src; 45 | --size; 46 | } 47 | } 48 | 49 | template 50 | inline T unalignedLoadEndian(const void * address) 51 | { 52 | T res {}; 53 | if constexpr (std::endian::native == endian) 54 | memcpy(&res, address, sizeof(res)); 55 | else 56 | reverseMemcpy(&res, address, sizeof(res)); 57 | return res; 58 | } 59 | 60 | 61 | template 62 | inline void unalignedStoreEndian(void * address, T & src) 63 | { 64 | static_assert(std::is_trivially_copyable_v); 65 | if constexpr (std::endian::native == endian) 66 | memcpy(address, &src, sizeof(src)); 67 | else 68 | reverseMemcpy(address, &src, sizeof(src)); 69 | } 70 | 71 | 72 | template 73 | inline T unalignedLoadLittleEndian(const void * address) 74 | { 75 | return unalignedLoadEndian(address); 76 | } 77 | 78 | 79 | template 80 | inline void unalignedStoreLittleEndian(void * address, 81 | const typename std::enable_if::type & src) 82 | { 83 | unalignedStoreEndian(address, src); 84 | } 85 | 86 | template 87 | inline T unalignedLoadBigEndian(const void * address) 88 | { 89 | return unalignedLoadEndian(address); 90 | } 91 | 92 | 93 | template 94 | inline void unalignedStoreBigEndian(void * address, 95 | const typename std::enable_if::type & src) 96 | { 97 | unalignedStoreEndian(address, src); 98 | } 99 | 100 | 101 | /** Hash functions that are better than the trivial function std::hash. 102 | * 103 | * Example: when we do aggregation by the visitor ID, the performance increase is more than 5 times. 104 | * This is because of following reasons: 105 | * - in Metrica web analytics system, visitor identifier is an integer that has timestamp with seconds resolution in lower bits; 106 | * - in typical implementation of standard library, hash function for integers is trivial and just use lower bits; 107 | * - traffic is non-uniformly distributed across a day; 108 | * - we are using open-addressing linear probing hash tables that are most critical to hash function quality, 109 | * and trivial hash function gives disastrous results. 110 | */ 111 | 112 | /** Taken from MurmurHash. This is Murmur finalizer. 113 | * Faster than intHash32 when inserting into the hash table UInt64 -> UInt64, where the key is the visitor ID. 114 | */ 115 | inline DB::UInt64 intHash64(DB::UInt64 x) 116 | { 117 | x ^= x >> 33; 118 | x *= 0xff51afd7ed558ccdULL; 119 | x ^= x >> 33; 120 | x *= 0xc4ceb9fe1a85ec53ULL; 121 | x ^= x >> 33; 122 | 123 | return x; 124 | } 125 | 126 | /** CRC32C is not very high-quality as a hash function, 127 | * according to avalanche and bit independence tests (see SMHasher software), as well as a small number of bits, 128 | * but can behave well when used in hash tables, 129 | * due to high speed (latency 3 + 1 clock cycle, throughput 1 clock cycle). 130 | * Works only with SSE 4.2 support. 131 | */ 132 | #ifdef __SSE4_2__ 133 | #include 134 | #endif 135 | 136 | #if defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) 137 | #include 138 | #endif 139 | 140 | #if (defined(__PPC64__) || defined(__powerpc64__)) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 141 | #include "vec_crc32.h" 142 | #endif 143 | 144 | #if defined(__s390x__) && __BYTE_ORDER__==__ORDER_BIG_ENDIAN__ 145 | #include 146 | 147 | inline uint32_t s390x_crc32_u8(uint32_t crc, uint8_t v) 148 | { 149 | return crc32_be(crc, reinterpret_cast(&v), sizeof(v)); 150 | } 151 | 152 | inline uint32_t s390x_crc32_u16(uint32_t crc, uint16_t v) 153 | { 154 | return crc32_be(crc, reinterpret_cast(&v), sizeof(v)); 155 | } 156 | 157 | inline uint32_t s390x_crc32_u32(uint32_t crc, uint32_t v) 158 | { 159 | return crc32_be(crc, reinterpret_cast(&v), sizeof(v)); 160 | } 161 | 162 | inline uint64_t s390x_crc32(uint64_t crc, uint64_t v) 163 | { 164 | uint64_t _crc = crc; 165 | uint32_t value_h, value_l; 166 | value_h = (v >> 32) & 0xffffffff; 167 | value_l = v & 0xffffffff; 168 | _crc = crc32_be(static_cast(_crc), reinterpret_cast(&value_h), sizeof(uint32_t)); 169 | _crc = crc32_be(static_cast(_crc), reinterpret_cast(&value_l), sizeof(uint32_t)); 170 | return _crc; 171 | } 172 | #endif 173 | 174 | /// NOTE: Intel intrinsic can be confusing. 175 | /// - https://code.google.com/archive/p/sse-intrinsics/wikis/PmovIntrinsicBug.wiki 176 | /// - https://stackoverflow.com/questions/15752770/mm-crc32-u64-poorly-defined 177 | inline DB::UInt64 intHashCRC32(DB::UInt64 x) 178 | { 179 | #ifdef __SSE4_2__ 180 | return _mm_crc32_u64(-1ULL, x); 181 | #elif defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) 182 | return __crc32cd(-1U, x); 183 | #elif (defined(__PPC64__) || defined(__powerpc64__)) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 184 | return crc32_ppc(-1U, reinterpret_cast(&x), sizeof(x)); 185 | #elif defined(__s390x__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 186 | return s390x_crc32(-1U, x); 187 | #else 188 | /// On other platforms we do not have CRC32. NOTE This can be confusing. 189 | /// NOTE: consider using intHash32() 190 | return intHash64(x); 191 | #endif 192 | } 193 | inline DB::UInt64 intHashCRC32(DB::UInt64 x, DB::UInt64 updated_value) 194 | { 195 | #ifdef __SSE4_2__ 196 | return _mm_crc32_u64(updated_value, x); 197 | #elif defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) 198 | return __crc32cd(static_cast(updated_value), x); 199 | #elif (defined(__PPC64__) || defined(__powerpc64__)) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 200 | return crc32_ppc(updated_value, reinterpret_cast(&x), sizeof(x)); 201 | #elif defined(__s390x__) && __BYTE_ORDER__==__ORDER_BIG_ENDIAN__ 202 | return s390x_crc32(updated_value, x); 203 | #else 204 | /// On other platforms we do not have CRC32. NOTE This can be confusing. 205 | return intHash64(x) ^ updated_value; 206 | #endif 207 | } 208 | 209 | template 210 | requires std::has_unique_object_representations_v && (sizeof(T) % sizeof(DB::UInt64) == 0) 211 | inline DB::UInt64 intHashCRC32(const T & x, DB::UInt64 updated_value) 212 | { 213 | const auto * begin = reinterpret_cast(&x); 214 | for (size_t i = 0; i < sizeof(T); i += sizeof(UInt64)) 215 | { 216 | updated_value = intHashCRC32(unalignedLoad(begin), updated_value); 217 | begin += sizeof(DB::UInt64); 218 | } 219 | 220 | return updated_value; 221 | } 222 | 223 | template 224 | requires(sizeof(T) <= sizeof(UInt64)) 225 | inline DB::UInt64 intHashCRC32(T x, DB::UInt64 updated_value) 226 | { 227 | static_assert(std::numeric_limits::is_iec559); 228 | 229 | // In IEEE 754, the only two floating point numbers that compare equal are 0.0 and -0.0. 230 | // See std::hash. 231 | if (x == static_cast(0.0)) 232 | return intHashCRC32(0, updated_value); 233 | 234 | UInt64 repr{}; 235 | 236 | if constexpr (sizeof(T) == sizeof(UInt32)) 237 | std::memcpy(&repr, &x, sizeof(UInt32)); 238 | else 239 | std::memcpy(&repr, &x, sizeof(UInt64)); 240 | 241 | return intHashCRC32(repr, updated_value); 242 | } 243 | 244 | inline UInt32 updateWeakHash32(const DB::UInt8 * pos, size_t size, DB::UInt32 updated_value) 245 | { 246 | if (size < 8) 247 | { 248 | UInt64 value = 0; 249 | 250 | switch (size) 251 | { 252 | case 0: 253 | break; 254 | case 1: 255 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 256 | __builtin_memcpy(&value, pos, 1); 257 | #else 258 | reverseMemcpy(&value, pos, 1); 259 | #endif 260 | break; 261 | case 2: 262 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 263 | __builtin_memcpy(&value, pos, 2); 264 | #else 265 | reverseMemcpy(&value, pos, 2); 266 | #endif 267 | break; 268 | case 3: 269 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 270 | __builtin_memcpy(&value, pos, 3); 271 | #else 272 | reverseMemcpy(&value, pos, 3); 273 | #endif 274 | break; 275 | case 4: 276 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 277 | __builtin_memcpy(&value, pos, 4); 278 | #else 279 | reverseMemcpy(&value, pos, 4); 280 | #endif 281 | break; 282 | case 5: 283 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 284 | __builtin_memcpy(&value, pos, 5); 285 | #else 286 | reverseMemcpy(&value, pos, 5); 287 | #endif 288 | break; 289 | case 6: 290 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 291 | __builtin_memcpy(&value, pos, 6); 292 | #else 293 | reverseMemcpy(&value, pos, 6); 294 | #endif 295 | break; 296 | case 7: 297 | #if __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__ 298 | __builtin_memcpy(&value, pos, 7); 299 | #else 300 | reverseMemcpy(&value, pos, 7); 301 | #endif 302 | break; 303 | default: 304 | __builtin_unreachable(); 305 | } 306 | 307 | reinterpret_cast(&value)[7] = size; 308 | return static_cast(intHashCRC32(value, updated_value)); 309 | } 310 | 311 | const auto * end = pos + size; 312 | while (pos + 8 <= end) 313 | { 314 | auto word = unalignedLoadLittleEndian(pos); 315 | updated_value = static_cast(intHashCRC32(word, updated_value)); 316 | 317 | pos += 8; 318 | } 319 | 320 | if (pos < end) 321 | { 322 | /// If string size is not divisible by 8. 323 | /// Lets' assume the string was 'abcdefghXYZ', so it's tail is 'XYZ'. 324 | DB::UInt8 tail_size = end - pos; 325 | /// Load tailing 8 bytes. Word is 'defghXYZ'. 326 | auto word = unalignedLoadLittleEndian(end - 8); 327 | /// Prepare mask which will set other 5 bytes to 0. It is 0xFFFFFFFFFFFFFFFF << 5 = 0xFFFFFF0000000000. 328 | /// word & mask = '\0\0\0\0\0XYZ' (bytes are reversed because of little ending) 329 | word &= (~UInt64(0)) << DB::UInt8(8 * (8 - tail_size)); 330 | /// Use least byte to store tail length. 331 | word |= tail_size; 332 | /// Now word is '\3\0\0\0\0XYZ' 333 | updated_value = static_cast(intHashCRC32(word, updated_value)); 334 | } 335 | 336 | return updated_value; 337 | } 338 | 339 | template 340 | requires (sizeof(T) <= sizeof(UInt64)) 341 | inline size_t DefaultHash64(T key) 342 | { 343 | DB::UInt64 out {0}; 344 | if constexpr (std::endian::native == std::endian::little) 345 | std::memcpy(&out, &key, sizeof(T)); 346 | else 347 | std::memcpy(reinterpret_cast(&out) + sizeof(DB::UInt64) - sizeof(T), &key, sizeof(T)); 348 | return intHash64(out); 349 | } 350 | 351 | 352 | // template 353 | // requires (sizeof(T) > sizeof(UInt64)) 354 | // inline size_t DefaultHash64(T key) 355 | // { 356 | // if constexpr (is_big_int_v && sizeof(T) == 16) 357 | // { 358 | // /// TODO This is classical antipattern. 359 | // return intHash64( 360 | // static_cast(key) ^ 361 | // static_cast(key >> 64)); 362 | // } 363 | // else if constexpr (std::is_same_v || std::is_same_v) 364 | // { 365 | // return intHash64( 366 | // static_cast(key.toUnderType()) ^ 367 | // static_cast(key.toUnderType() >> 64)); 368 | // } 369 | // else if constexpr (is_big_int_v && sizeof(T) == 32) 370 | // { 371 | // return intHash64( 372 | // static_cast(key) ^ 373 | // static_cast(key >> 64) ^ 374 | // static_cast(key >> 128) ^ 375 | // static_cast(key >> 256)); 376 | // } 377 | // UNREACHABLE(); 378 | // } 379 | 380 | template 381 | struct DefaultHash 382 | { 383 | size_t operator() (T key) const 384 | { 385 | return DefaultHash64(key); 386 | } 387 | }; 388 | 389 | // template 390 | // struct DefaultHash 391 | // { 392 | // size_t operator() (T key) const 393 | // { 394 | // return DefaultHash64(key.value); 395 | // } 396 | // }; 397 | 398 | template struct HashCRC32; 399 | 400 | template 401 | requires (sizeof(T) <= sizeof(UInt64)) 402 | inline size_t hashCRC32(T key, DB::UInt64 updated_value = -1) 403 | { 404 | DB::UInt64 out {0}; 405 | std::memcpy(&out, &key, sizeof(T)); 406 | return intHashCRC32(out, updated_value); 407 | } 408 | 409 | template 410 | requires (sizeof(T) > sizeof(UInt64)) 411 | inline size_t hashCRC32(T key, DB::UInt64 updated_value = -1) 412 | { 413 | return intHashCRC32(key, updated_value); 414 | } 415 | 416 | #define DEFINE_HASH(T) \ 417 | template <> struct HashCRC32\ 418 | {\ 419 | size_t operator() (T key) const\ 420 | {\ 421 | return hashCRC32(key);\ 422 | }\ 423 | }; 424 | 425 | DEFINE_HASH(DB::UInt8) 426 | DEFINE_HASH(DB::UInt16) 427 | DEFINE_HASH(DB::UInt32) 428 | DEFINE_HASH(DB::UInt64) 429 | // DEFINE_HASH(DB::UInt128) 430 | // DEFINE_HASH(DB::UInt256) 431 | DEFINE_HASH(DB::Int8) 432 | DEFINE_HASH(DB::Int16) 433 | DEFINE_HASH(DB::Int32) 434 | DEFINE_HASH(DB::Int64) 435 | // DEFINE_HASH(DB::Int128) 436 | // DEFINE_HASH(DB::Int256) 437 | DEFINE_HASH(DB::Float32) 438 | DEFINE_HASH(DB::Float64) 439 | // DEFINE_HASH(DB::UUID) 440 | // DEFINE_HASH(DB::IPv4) 441 | // DEFINE_HASH(DB::IPv6) 442 | 443 | #undef DEFINE_HASH 444 | 445 | 446 | // struct UInt128Hash 447 | // { 448 | // size_t operator()(UInt128 x) const 449 | // { 450 | // return CityHash_v1_0_2::Hash128to64({x.items[0], x.items[1]}); 451 | // } 452 | // }; 453 | 454 | // struct UUIDHash 455 | // { 456 | // size_t operator()(DB::UUID x) const 457 | // { 458 | // return UInt128Hash()(x.toUnderType()); 459 | // } 460 | // }; 461 | 462 | #ifdef __SSE4_2__ 463 | 464 | // struct UInt128HashCRC32 465 | // { 466 | // size_t operator()(UInt128 x) const 467 | // { 468 | // UInt64 crc = -1ULL; 469 | // crc = _mm_crc32_u64(crc, x.items[0]); 470 | // crc = _mm_crc32_u64(crc, x.items[1]); 471 | // return crc; 472 | // } 473 | // }; 474 | 475 | #elif defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) 476 | 477 | // struct UInt128HashCRC32 478 | // { 479 | // size_t operator()(UInt128 x) const 480 | // { 481 | // UInt64 crc = -1ULL; 482 | // crc = __crc32cd(static_cast(crc), x.items[0]); 483 | // crc = __crc32cd(static_cast(crc), x.items[1]); 484 | // return crc; 485 | // } 486 | // }; 487 | 488 | #else 489 | 490 | /// On other platforms we do not use CRC32. NOTE This can be confusing. 491 | // struct UInt128HashCRC32 : public UInt128Hash {}; 492 | 493 | #endif 494 | 495 | // struct UInt128TrivialHash 496 | // { 497 | // size_t operator()(UInt128 x) const { return x.items[0]; } 498 | // }; 499 | 500 | // struct UUIDTrivialHash 501 | // { 502 | // size_t operator()(DB::UUID x) const { return x.toUnderType().items[0]; } 503 | // }; 504 | 505 | // struct UInt256Hash 506 | // { 507 | // size_t operator()(UInt256 x) const 508 | // { 509 | // /// NOTE suboptimal 510 | // return CityHash_v1_0_2::Hash128to64({ 511 | // CityHash_v1_0_2::Hash128to64({x.items[0], x.items[1]}), 512 | // CityHash_v1_0_2::Hash128to64({x.items[2], x.items[3]})}); 513 | // } 514 | // }; 515 | 516 | #ifdef __SSE4_2__ 517 | 518 | // struct UInt256HashCRC32 519 | // { 520 | // size_t operator()(UInt256 x) const 521 | // { 522 | // UInt64 crc = -1ULL; 523 | // crc = _mm_crc32_u64(crc, x.items[0]); 524 | // crc = _mm_crc32_u64(crc, x.items[1]); 525 | // crc = _mm_crc32_u64(crc, x.items[2]); 526 | // crc = _mm_crc32_u64(crc, x.items[3]); 527 | // return crc; 528 | // } 529 | // }; 530 | 531 | #elif defined(__aarch64__) && defined(__ARM_FEATURE_CRC32) 532 | 533 | // struct UInt256HashCRC32 534 | // { 535 | // size_t operator()(UInt256 x) const 536 | // { 537 | // UInt64 crc = -1ULL; 538 | // crc = __crc32cd(static_cast(crc), x.items[0]); 539 | // crc = __crc32cd(static_cast(crc), x.items[1]); 540 | // crc = __crc32cd(static_cast(crc), x.items[2]); 541 | // crc = __crc32cd(static_cast(crc), x.items[3]); 542 | // return crc; 543 | // } 544 | // }; 545 | 546 | #else 547 | 548 | /// We do not need to use CRC32 on other platforms. NOTE This can be confusing. 549 | // struct UInt256HashCRC32 : public UInt256Hash {}; 550 | 551 | #endif 552 | 553 | // template <> 554 | // struct DefaultHash : public UInt128Hash {}; 555 | 556 | // template <> 557 | // struct DefaultHash : public UInt256Hash {}; 558 | 559 | // template <> 560 | // struct DefaultHash : public UUIDHash {}; 561 | 562 | 563 | /// It is reasonable to use for UInt8, UInt16 with sufficient hash table size. 564 | struct TrivialHash 565 | { 566 | template 567 | size_t operator() (T key) const 568 | { 569 | return key; 570 | } 571 | }; 572 | 573 | 574 | /** A relatively good non-cryptographic hash function from UInt64 to UInt32. 575 | * But worse (both in quality and speed) than just cutting intHash64. 576 | * Taken from here: http://www.concentric.net/~ttwang/tech/inthash.htm 577 | * 578 | * Slightly changed compared to the function by link: shifts to the right are accidentally replaced by a cyclic shift to the right. 579 | * This change did not affect the smhasher test results. 580 | * 581 | * It is recommended to use different salt for different tasks. 582 | * That was the case that in the database values were sorted by hash (for low-quality pseudo-random spread), 583 | * and in another place, in the aggregate function, the same hash was used in the hash table, 584 | * as a result, this aggregate function was monstrously slowed due to collisions. 585 | * 586 | * NOTE Salting is far from perfect, because it commutes with first steps of calculation. 587 | * 588 | * NOTE As mentioned, this function is slower than intHash64. 589 | * But occasionally, it is faster, when written in a loop and loop is vectorized. 590 | */ 591 | template 592 | inline DB::UInt32 intHash32(DB::UInt64 key) 593 | { 594 | key ^= salt; 595 | 596 | key = (~key) + (key << 18); 597 | key = key ^ ((key >> 31) | (key << 33)); 598 | key = key * 21; 599 | key = key ^ ((key >> 11) | (key << 53)); 600 | key = key + (key << 6); 601 | key = key ^ ((key >> 22) | (key << 42)); 602 | 603 | return static_cast(key); 604 | } 605 | 606 | 607 | /// For containers. 608 | template 609 | struct IntHash32 610 | { 611 | size_t operator() (const T & key) const 612 | { 613 | // if constexpr (is_big_int_v && sizeof(T) == 16) 614 | // { 615 | // return intHash32(key.items[0] ^ key.items[1]); 616 | // } 617 | // else if constexpr (is_big_int_v && sizeof(T) == 32) 618 | // { 619 | // return intHash32(key.items[0] ^ key.items[1] ^ key.items[2] ^ key.items[3]); 620 | // } 621 | // else 622 | static_assert (sizeof(T) <= sizeof(UInt64)); 623 | 624 | { 625 | DB::UInt64 out {0}; 626 | std::memcpy(&out, &key, sizeof(T)); 627 | return intHash32(out); 628 | } 629 | 630 | // UNREACHABLE(); 631 | } 632 | }; 633 | 634 | /// TODO Strings 635 | 636 | // template <> 637 | // struct DefaultHash : public StringRefHash {}; 638 | -------------------------------------------------------------------------------- /Results.md: -------------------------------------------------------------------------------- 1 | # Results 2 | 3 | Results for different platforms. 4 | 5 | ## X86-64 results on c6a.4xlarge: 6 | 7 | ``` 8 | File: data/WatchID.bin 9 | Key type: Int64 10 | Keys size: 99997497 11 | Unique keys size: 99997493 12 | +------------------------------+-----------------+---------------+--------------+ 13 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 14 | +------------------------------+-----------------+---------------+--------------+ 15 | | ClickHouse HashMap | std::hash | 6.23 | 4.00 GiB | 16 | | ClickHouse HashMap | ClickHouse hash | 7.36 | 4.00 GiB | 17 | | ClickHouse HashMap | absl::Hash | 6.70 | 4.00 GiB | 18 | | absl::flat_hash_map | std::hash | 9.92 | 2.13 GiB | 19 | | absl::flat_hash_map | ClickHouse hash | 10.26 | 2.13 GiB | 20 | | absl::flat_hash_map | absl::Hash | 10.01 | 2.13 GiB | 21 | | google::dense_hash_map | std::hash | 9.80 | 4.00 GiB | 22 | | google::dense_hash_map | ClickHouse hash | 10.18 | 4.00 GiB | 23 | | google::dense_hash_map | absl::Hash | 9.88 | 4.00 GiB | 24 | | tsl::hopscotch_map | std::hash | 14.76 | 3.00 GiB | 25 | | tsl::hopscotch_map | ClickHouse hash | 16.31 | 3.00 GiB | 26 | | tsl::hopscotch_map | absl::Hash | 16.02 | 3.00 GiB | 27 | | ankerl::unordered_dense::map | std::hash | 12.43 | 2.49 GiB | 28 | | ankerl::unordered_dense::map | ClickHouse hash | 13.63 | 2.49 GiB | 29 | | ankerl::unordered_dense::map | absl::Hash | 12.93 | 2.49 GiB | 30 | | ska::flat_hash_map | std::hash | 10.74 | 6.00 GiB | 31 | | ska::flat_hash_map | ClickHouse hash | 11.95 | 6.00 GiB | 32 | | ska::flat_hash_map | absl::Hash | 10.95 | 6.00 GiB | 33 | | ska::bytell_hash_map | std::hash | 15.12 | 2.13 GiB | 34 | | ska::bytell_hash_map | ClickHouse hash | 15.55 | 2.13 GiB | 35 | | ska::bytell_hash_map | absl::Hash | 15.04 | 2.13 GiB | 36 | | std::unordered_map | std::hash | 55.17 | 3.74 GiB | 37 | | std::unordered_map | ClickHouse hash | 58.37 | 5.23 GiB | 38 | | std::unordered_map | absl::Hash | 58.03 | 5.23 GiB | 39 | +------------------------------+-----------------+---------------+--------------+ 40 | 41 | File: data/URLHash.bin 42 | Key type: Int64 43 | Keys size: 99997497 44 | Unique keys size: 20714865 45 | +------------------------------+-----------------+---------------+--------------+ 46 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 47 | +------------------------------+-----------------+---------------+--------------+ 48 | | ClickHouse HashMap | std::hash | 2.25 | 1.00 GiB | 49 | | ClickHouse HashMap | ClickHouse hash | 2.79 | 1.00 GiB | 50 | | ClickHouse HashMap | absl::Hash | 2.48 | 1.00 GiB | 51 | | absl::flat_hash_map | std::hash | 3.09 | 544.62 MiB | 52 | | absl::flat_hash_map | ClickHouse hash | 3.43 | 544.58 MiB | 53 | | absl::flat_hash_map | absl::Hash | 3.26 | 544.55 MiB | 54 | | google::dense_hash_map | std::hash | 3.12 | 1.00 GiB | 55 | | google::dense_hash_map | ClickHouse hash | 3.48 | 1.00 GiB | 56 | | google::dense_hash_map | absl::Hash | 3.32 | 1.00 GiB | 57 | | tsl::hopscotch_map | std::hash | 3.95 | 768.57 MiB | 58 | | tsl::hopscotch_map | ClickHouse hash | 4.76 | 768.64 MiB | 59 | | tsl::hopscotch_map | absl::Hash | 4.50 | 768.61 MiB | 60 | | ankerl::unordered_dense::map | std::hash | 3.92 | 572.65 MiB | 61 | | ankerl::unordered_dense::map | ClickHouse hash | 4.39 | 572.65 MiB | 62 | | ankerl::unordered_dense::map | absl::Hash | 4.13 | 572.62 MiB | 63 | | ska::flat_hash_map | std::hash | 2.89 | 1.50 GiB | 64 | | ska::flat_hash_map | ClickHouse hash | 3.38 | 1.50 GiB | 65 | | ska::flat_hash_map | absl::Hash | 3.09 | 1.50 GiB | 66 | | ska::bytell_hash_map | std::hash | 3.73 | 544.57 MiB | 67 | | ska::bytell_hash_map | ClickHouse hash | 4.24 | 544.57 MiB | 68 | | ska::bytell_hash_map | absl::Hash | 4.02 | 544.57 MiB | 69 | | std::unordered_map | std::hash | 9.34 | 820.18 MiB | 70 | | std::unordered_map | ClickHouse hash | 10.77 | 1.11 GiB | 71 | | std::unordered_map | absl::Hash | 10.80 | 1.11 GiB | 72 | +------------------------------+-----------------+---------------+--------------+ 73 | 74 | File: data/UserID.bin 75 | Key type: Int64 76 | Keys size: 99997497 77 | Unique keys size: 17630976 78 | +------------------------------+-----------------+---------------+--------------+ 79 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 80 | +------------------------------+-----------------+---------------+--------------+ 81 | | ClickHouse HashMap | std::hash | 1.87 | 1.00 GiB | 82 | | ClickHouse HashMap | ClickHouse hash | 2.27 | 1.00 GiB | 83 | | ClickHouse HashMap | absl::Hash | 2.03 | 1.00 GiB | 84 | | absl::flat_hash_map | std::hash | 2.55 | 544.60 MiB | 85 | | absl::flat_hash_map | ClickHouse hash | 2.82 | 544.61 MiB | 86 | | absl::flat_hash_map | absl::Hash | 2.65 | 544.60 MiB | 87 | | google::dense_hash_map | std::hash | 2.59 | 1.00 GiB | 88 | | google::dense_hash_map | ClickHouse hash | 2.86 | 1.00 GiB | 89 | | google::dense_hash_map | absl::Hash | 2.70 | 1.00 GiB | 90 | | tsl::hopscotch_map | std::hash | 3.10 | 768.57 MiB | 91 | | tsl::hopscotch_map | ClickHouse hash | 3.65 | 768.64 MiB | 92 | | tsl::hopscotch_map | absl::Hash | 3.59 | 768.64 MiB | 93 | | ankerl::unordered_dense::map | std::hash | 3.04 | 525.47 MiB | 94 | | ankerl::unordered_dense::map | ClickHouse hash | 3.45 | 525.47 MiB | 95 | | ankerl::unordered_dense::map | absl::Hash | 3.17 | 525.43 MiB | 96 | | ska::flat_hash_map | std::hash | 2.48 | 1.50 GiB | 97 | | ska::flat_hash_map | ClickHouse hash | 2.87 | 1.50 GiB | 98 | | ska::flat_hash_map | absl::Hash | 2.64 | 1.50 GiB | 99 | | ska::bytell_hash_map | std::hash | 3.07 | 544.57 MiB | 100 | | ska::bytell_hash_map | ClickHouse hash | 3.33 | 544.57 MiB | 101 | | ska::bytell_hash_map | absl::Hash | 3.24 | 544.55 MiB | 102 | | std::unordered_map | std::hash | 7.84 | 726.08 MiB | 103 | | std::unordered_map | ClickHouse hash | 8.91 | 995.03 MiB | 104 | | std::unordered_map | absl::Hash | 8.81 | 995.00 MiB | 105 | +------------------------------+-----------------+---------------+--------------+ 106 | 107 | File: data/RegionID.bin 108 | Key type: Int32 109 | Keys size: 99997497 110 | Unique keys size: 9040 111 | +------------------------------+-----------------+---------------+--------------+ 112 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 113 | +------------------------------+-----------------+---------------+--------------+ 114 | | ClickHouse HashMap | std::hash | 0.14 | 1.16 MiB | 115 | | ClickHouse HashMap | ClickHouse hash | 0.21 | 1.18 MiB | 116 | | ClickHouse HashMap | absl::Hash | 0.17 | 1.15 MiB | 117 | | absl::flat_hash_map | std::hash | 2.98 | 604.00 KiB | 118 | | absl::flat_hash_map | ClickHouse hash | 0.36 | 728.00 KiB | 119 | | absl::flat_hash_map | absl::Hash | 0.32 | 720.00 KiB | 120 | | google::dense_hash_map | std::hash | 0.34 | 936.00 KiB | 121 | | google::dense_hash_map | ClickHouse hash | 0.38 | 936.00 KiB | 122 | | google::dense_hash_map | absl::Hash | 0.32 | 936.00 KiB | 123 | | tsl::hopscotch_map | std::hash | 0.97 | 812.00 KiB | 124 | | tsl::hopscotch_map | ClickHouse hash | 0.32 | 812.00 KiB | 125 | | tsl::hopscotch_map | absl::Hash | 0.34 | 876.00 KiB | 126 | | ankerl::unordered_dense::map | std::hash | 0.46 | 560.00 KiB | 127 | | ankerl::unordered_dense::map | ClickHouse hash | 0.55 | 624.00 KiB | 128 | | ankerl::unordered_dense::map | absl::Hash | 0.52 | 560.00 KiB | 129 | | ska::flat_hash_map | std::hash | 0.16 | 1.17 MiB | 130 | | ska::flat_hash_map | ClickHouse hash | 0.26 | 1.17 MiB | 131 | | ska::flat_hash_map | absl::Hash | 0.23 | 1.17 MiB | 132 | | ska::bytell_hash_map | std::hash | 0.23 | 700.00 KiB | 133 | | ska::bytell_hash_map | ClickHouse hash | 0.32 | 700.00 KiB | 134 | | ska::bytell_hash_map | absl::Hash | 0.28 | 700.00 KiB | 135 | | std::unordered_map | std::hash | 0.46 | 500.00 KiB | 136 | | std::unordered_map | ClickHouse hash | 0.65 | 764.00 KiB | 137 | | std::unordered_map | absl::Hash | 0.64 | 764.00 KiB | 138 | +------------------------------+-----------------+---------------+--------------+ 139 | 140 | File: data/CounterID.bin 141 | Key type: Int32 142 | Keys size: 99997497 143 | Unique keys size: 6506 144 | +------------------------------+-----------------+---------------+--------------+ 145 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 146 | +------------------------------+-----------------+---------------+--------------+ 147 | | ClickHouse HashMap | std::hash | 0.11 | 440.00 KiB | 148 | | ClickHouse HashMap | ClickHouse hash | 0.19 | 476.00 KiB | 149 | | ClickHouse HashMap | absl::Hash | 0.14 | 496.00 KiB | 150 | | absl::flat_hash_map | std::hash | 1.28 | 528.00 KiB | 151 | | absl::flat_hash_map | ClickHouse hash | 0.35 | 572.00 KiB | 152 | | absl::flat_hash_map | absl::Hash | 0.32 | 604.00 KiB | 153 | | google::dense_hash_map | std::hash | 0.35 | 656.00 KiB | 154 | | google::dense_hash_map | ClickHouse hash | 0.38 | 680.00 KiB | 155 | | google::dense_hash_map | absl::Hash | 0.32 | 680.00 KiB | 156 | | tsl::hopscotch_map | std::hash | 0.23 | 620.00 KiB | 157 | | tsl::hopscotch_map | ClickHouse hash | 0.43 | 684.00 KiB | 158 | | tsl::hopscotch_map | absl::Hash | 0.49 | 684.00 KiB | 159 | | ankerl::unordered_dense::map | std::hash | 0.34 | 500.00 KiB | 160 | | ankerl::unordered_dense::map | ClickHouse hash | 0.40 | 500.00 KiB | 161 | | ankerl::unordered_dense::map | absl::Hash | 0.37 | 500.00 KiB | 162 | | ska::flat_hash_map | std::hash | 0.12 | 812.00 KiB | 163 | | ska::flat_hash_map | ClickHouse hash | 0.17 | 812.00 KiB | 164 | | ska::flat_hash_map | absl::Hash | 0.16 | 812.00 KiB | 165 | | ska::bytell_hash_map | std::hash | 0.28 | 564.00 KiB | 166 | | ska::bytell_hash_map | ClickHouse hash | 0.40 | 564.00 KiB | 167 | | ska::bytell_hash_map | absl::Hash | 0.37 | 564.00 KiB | 168 | | std::unordered_map | std::hash | 0.20 | 500.00 KiB | 169 | | std::unordered_map | ClickHouse hash | 0.38 | 500.00 KiB | 170 | | std::unordered_map | absl::Hash | 0.37 | 500.00 KiB | 171 | +------------------------------+-----------------+---------------+--------------+ 172 | 173 | File: data/TraficSourceID.bin 174 | Key type: Int16 175 | Keys size: 99997497 176 | Unique keys size: 10 177 | +------------------------------+-----------------+---------------+--------------+ 178 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 179 | +------------------------------+-----------------+---------------+--------------+ 180 | | ClickHouse HashMap | std::hash | 0.21 | 4.00 KiB | 181 | | ClickHouse HashMap | ClickHouse hash | 0.27 | 4.00 KiB | 182 | | ClickHouse HashMap | absl::Hash | 0.23 | 4.00 KiB | 183 | | absl::flat_hash_map | std::hash | 0.27 | 4.00 KiB | 184 | | absl::flat_hash_map | ClickHouse hash | 0.35 | 4.00 KiB | 185 | | absl::flat_hash_map | absl::Hash | 0.30 | 4.00 KiB | 186 | | google::dense_hash_map | std::hash | 0.34 | 4.00 KiB | 187 | | google::dense_hash_map | ClickHouse hash | 0.50 | 4.00 KiB | 188 | | google::dense_hash_map | absl::Hash | 0.31 | 4.00 KiB | 189 | | tsl::hopscotch_map | std::hash | 0.14 | 4.00 KiB | 190 | | tsl::hopscotch_map | ClickHouse hash | 0.51 | 4.00 KiB | 191 | | tsl::hopscotch_map | absl::Hash | 0.28 | 4.00 KiB | 192 | | ankerl::unordered_dense::map | std::hash | 0.31 | 4.00 KiB | 193 | | ankerl::unordered_dense::map | ClickHouse hash | 0.59 | 4.00 KiB | 194 | | ankerl::unordered_dense::map | absl::Hash | 0.65 | 4.00 KiB | 195 | | ska::flat_hash_map | std::hash | 0.13 | 4.00 KiB | 196 | | ska::flat_hash_map | ClickHouse hash | 0.18 | 4.00 KiB | 197 | | ska::flat_hash_map | absl::Hash | 0.26 | 4.00 KiB | 198 | | ska::bytell_hash_map | std::hash | 0.20 | 4.00 KiB | 199 | | ska::bytell_hash_map | ClickHouse hash | 0.32 | 4.00 KiB | 200 | | ska::bytell_hash_map | absl::Hash | 0.37 | 4.00 KiB | 201 | | std::unordered_map | std::hash | 0.41 | 4.00 KiB | 202 | | std::unordered_map | ClickHouse hash | 0.77 | 4.00 KiB | 203 | | std::unordered_map | absl::Hash | 0.66 | 4.00 KiB | 204 | +------------------------------+-----------------+---------------+--------------+ 205 | 206 | File: data/AdvEngineID.bin 207 | Key type: Int16 208 | Keys size: 99997497 209 | Unique keys size: 19 210 | +------------------------------+-----------------+---------------+--------------+ 211 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 212 | +------------------------------+-----------------+---------------+--------------+ 213 | | ClickHouse HashMap | std::hash | 0.06 | 4.00 KiB | 214 | | ClickHouse HashMap | ClickHouse hash | 0.09 | 4.00 KiB | 215 | | ClickHouse HashMap | absl::Hash | 0.09 | 4.00 KiB | 216 | | absl::flat_hash_map | std::hash | 0.26 | 4.00 KiB | 217 | | absl::flat_hash_map | ClickHouse hash | 0.35 | 4.00 KiB | 218 | | absl::flat_hash_map | absl::Hash | 0.31 | 4.00 KiB | 219 | | google::dense_hash_map | std::hash | 0.34 | 4.00 KiB | 220 | | google::dense_hash_map | ClickHouse hash | 0.31 | 4.00 KiB | 221 | | google::dense_hash_map | absl::Hash | 0.31 | 4.00 KiB | 222 | | tsl::hopscotch_map | std::hash | 0.14 | 4.00 KiB | 223 | | tsl::hopscotch_map | ClickHouse hash | 0.33 | 4.00 KiB | 224 | | tsl::hopscotch_map | absl::Hash | 0.31 | 4.00 KiB | 225 | | ankerl::unordered_dense::map | std::hash | 0.32 | 4.00 KiB | 226 | | ankerl::unordered_dense::map | ClickHouse hash | 0.41 | 4.00 KiB | 227 | | ankerl::unordered_dense::map | absl::Hash | 0.32 | 4.00 KiB | 228 | | ska::flat_hash_map | std::hash | 0.11 | 4.00 KiB | 229 | | ska::flat_hash_map | ClickHouse hash | 0.16 | 4.00 KiB | 230 | | ska::flat_hash_map | absl::Hash | 0.16 | 4.00 KiB | 231 | | ska::bytell_hash_map | std::hash | 0.20 | 4.00 KiB | 232 | | ska::bytell_hash_map | ClickHouse hash | 0.29 | 4.00 KiB | 233 | | ska::bytell_hash_map | absl::Hash | 0.26 | 4.00 KiB | 234 | | std::unordered_map | std::hash | 0.27 | 4.00 KiB | 235 | | std::unordered_map | ClickHouse hash | 0.58 | 4.00 KiB | 236 | | std::unordered_map | absl::Hash | 0.78 | 4.00 KiB | 237 | +------------------------------+-----------------+---------------+--------------+ 238 | ``` 239 | 240 | ## ARM results on m7g.4xlarge: 241 | 242 | ``` 243 | File: data/WatchID.bin 244 | Key type: Int64 245 | Keys size: 99997497 246 | Unique keys size: 99997493 247 | +------------------------------+-----------------+---------------+--------------+ 248 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 249 | +------------------------------+-----------------+---------------+--------------+ 250 | | ClickHouse HashMap | std::hash | 6.50 | 4.00 GiB | 251 | | ClickHouse HashMap | ClickHouse hash | 6.88 | 4.00 GiB | 252 | | ClickHouse HashMap | absl::Hash | 6.87 | 4.00 GiB | 253 | | absl::flat_hash_map | std::hash | 9.21 | 2.13 GiB | 254 | | absl::flat_hash_map | ClickHouse hash | 9.73 | 2.13 GiB | 255 | | absl::flat_hash_map | absl::Hash | 9.49 | 2.13 GiB | 256 | | google::dense_hash_map | std::hash | 8.86 | 4.00 GiB | 257 | | google::dense_hash_map | ClickHouse hash | 10.32 | 4.00 GiB | 258 | | google::dense_hash_map | absl::Hash | 10.26 | 4.00 GiB | 259 | | tsl::hopscotch_map | std::hash | 16.00 | 3.00 GiB | 260 | | tsl::hopscotch_map | ClickHouse hash | 16.99 | 3.00 GiB | 261 | | tsl::hopscotch_map | absl::Hash | 16.60 | 3.00 GiB | 262 | | ankerl::unordered_dense::map | std::hash | 14.83 | 2.49 GiB | 263 | | ankerl::unordered_dense::map | ClickHouse hash | 16.72 | 2.49 GiB | 264 | | ankerl::unordered_dense::map | absl::Hash | 16.88 | 2.49 GiB | 265 | | ska::flat_hash_map | std::hash | 10.56 | 6.00 GiB | 266 | | ska::flat_hash_map | ClickHouse hash | 10.93 | 6.00 GiB | 267 | | ska::flat_hash_map | absl::Hash | 10.81 | 6.00 GiB | 268 | | ska::bytell_hash_map | std::hash | 14.73 | 2.13 GiB | 269 | | ska::bytell_hash_map | ClickHouse hash | 15.29 | 2.13 GiB | 270 | | ska::bytell_hash_map | absl::Hash | 14.37 | 2.13 GiB | 271 | | std::unordered_map | std::hash | 61.47 | 3.74 GiB | 272 | | std::unordered_map | ClickHouse hash | 65.53 | 5.23 GiB | 273 | | std::unordered_map | absl::Hash | 64.34 | 5.23 GiB | 274 | +------------------------------+-----------------+---------------+--------------+ 275 | 276 | File: data/URLHash.bin 277 | Key type: Int64 278 | Keys size: 99997497 279 | Unique keys size: 20714865 280 | +------------------------------+-----------------+---------------+--------------+ 281 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 282 | +------------------------------+-----------------+---------------+--------------+ 283 | | ClickHouse HashMap | std::hash | 2.32 | 1.00 GiB | 284 | | ClickHouse HashMap | ClickHouse hash | 2.67 | 1.00 GiB | 285 | | ClickHouse HashMap | absl::Hash | 2.56 | 1.00 GiB | 286 | | absl::flat_hash_map | std::hash | 3.58 | 544.73 MiB | 287 | | absl::flat_hash_map | ClickHouse hash | 3.08 | 544.67 MiB | 288 | | absl::flat_hash_map | absl::Hash | 3.07 | 544.67 MiB | 289 | | google::dense_hash_map | std::hash | 3.08 | 1.00 GiB | 290 | | google::dense_hash_map | ClickHouse hash | 3.65 | 1.00 GiB | 291 | | google::dense_hash_map | absl::Hash | 3.62 | 1.00 GiB | 292 | | tsl::hopscotch_map | std::hash | 4.23 | 768.67 MiB | 293 | | tsl::hopscotch_map | ClickHouse hash | 5.07 | 768.67 MiB | 294 | | tsl::hopscotch_map | absl::Hash | 4.84 | 768.67 MiB | 295 | | ankerl::unordered_dense::map | std::hash | 4.58 | 572.75 MiB | 296 | | ankerl::unordered_dense::map | ClickHouse hash | 5.08 | 572.68 MiB | 297 | | ankerl::unordered_dense::map | absl::Hash | 4.99 | 572.68 MiB | 298 | | ska::flat_hash_map | std::hash | 3.18 | 1.50 GiB | 299 | | ska::flat_hash_map | ClickHouse hash | 3.48 | 1.50 GiB | 300 | | ska::flat_hash_map | absl::Hash | 3.38 | 1.50 GiB | 301 | | ska::bytell_hash_map | std::hash | 3.55 | 544.67 MiB | 302 | | ska::bytell_hash_map | ClickHouse hash | 4.69 | 544.61 MiB | 303 | | ska::bytell_hash_map | absl::Hash | 3.92 | 544.61 MiB | 304 | | std::unordered_map | std::hash | 11.12 | 820.28 MiB | 305 | | std::unordered_map | ClickHouse hash | 12.56 | 1.11 GiB | 306 | | std::unordered_map | absl::Hash | 13.18 | 1.11 GiB | 307 | +------------------------------+-----------------+---------------+--------------+ 308 | 309 | File: data/UserID.bin 310 | Key type: Int64 311 | Keys size: 99997497 312 | Unique keys size: 17630976 313 | +------------------------------+-----------------+---------------+--------------+ 314 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 315 | +------------------------------+-----------------+---------------+--------------+ 316 | | ClickHouse HashMap | std::hash | 2.08 | 1.00 GiB | 317 | | ClickHouse HashMap | ClickHouse hash | 2.26 | 1.00 GiB | 318 | | ClickHouse HashMap | absl::Hash | 2.25 | 1.00 GiB | 319 | | absl::flat_hash_map | std::hash | 2.50 | 544.73 MiB | 320 | | absl::flat_hash_map | ClickHouse hash | 2.62 | 544.67 MiB | 321 | | absl::flat_hash_map | absl::Hash | 2.58 | 544.67 MiB | 322 | | google::dense_hash_map | std::hash | 2.56 | 1.00 GiB | 323 | | google::dense_hash_map | ClickHouse hash | 2.92 | 1.00 GiB | 324 | | google::dense_hash_map | absl::Hash | 2.92 | 1.00 GiB | 325 | | tsl::hopscotch_map | std::hash | 3.24 | 768.67 MiB | 326 | | tsl::hopscotch_map | ClickHouse hash | 3.88 | 768.67 MiB | 327 | | tsl::hopscotch_map | absl::Hash | 3.80 | 768.67 MiB | 328 | | ankerl::unordered_dense::map | std::hash | 3.49 | 525.57 MiB | 329 | | ankerl::unordered_dense::map | ClickHouse hash | 3.87 | 525.50 MiB | 330 | | ankerl::unordered_dense::map | absl::Hash | 3.86 | 525.50 MiB | 331 | | ska::flat_hash_map | std::hash | 2.75 | 1.50 GiB | 332 | | ska::flat_hash_map | ClickHouse hash | 3.02 | 1.50 GiB | 333 | | ska::flat_hash_map | absl::Hash | 2.98 | 1.50 GiB | 334 | | ska::bytell_hash_map | std::hash | 2.90 | 544.67 MiB | 335 | | ska::bytell_hash_map | ClickHouse hash | 3.58 | 544.61 MiB | 336 | | ska::bytell_hash_map | absl::Hash | 3.12 | 544.61 MiB | 337 | | std::unordered_map | std::hash | 9.18 | 726.18 MiB | 338 | | std::unordered_map | ClickHouse hash | 10.56 | 995.06 MiB | 339 | | std::unordered_map | absl::Hash | 10.35 | 995.06 MiB | 340 | +------------------------------+-----------------+---------------+--------------+ 341 | 342 | File: data/RegionID.bin 343 | Key type: Int32 344 | Keys size: 99997497 345 | Unique keys size: 9040 346 | +------------------------------+-----------------+---------------+--------------+ 347 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 348 | +------------------------------+-----------------+---------------+--------------+ 349 | | ClickHouse HashMap | std::hash | 0.17 | 1.22 MiB | 350 | | ClickHouse HashMap | ClickHouse hash | 0.20 | 1.22 MiB | 351 | | ClickHouse HashMap | absl::Hash | 0.21 | 1.22 MiB | 352 | | absl::flat_hash_map | std::hash | 5.27 | 764.00 KiB | 353 | | absl::flat_hash_map | ClickHouse hash | 0.31 | 860.00 KiB | 354 | | absl::flat_hash_map | absl::Hash | 0.32 | 864.00 KiB | 355 | | google::dense_hash_map | std::hash | 0.27 | 1.01 MiB | 356 | | google::dense_hash_map | ClickHouse hash | 0.33 | 1.01 MiB | 357 | | google::dense_hash_map | absl::Hash | 0.33 | 1.01 MiB | 358 | | tsl::hopscotch_map | std::hash | 1.34 | 912.00 KiB | 359 | | tsl::hopscotch_map | ClickHouse hash | 0.34 | 912.00 KiB | 360 | | tsl::hopscotch_map | absl::Hash | 0.34 | 976.00 KiB | 361 | | ankerl::unordered_dense::map | std::hash | 0.55 | 660.00 KiB | 362 | | ankerl::unordered_dense::map | ClickHouse hash | 0.70 | 660.00 KiB | 363 | | ankerl::unordered_dense::map | absl::Hash | 0.73 | 660.00 KiB | 364 | | ska::flat_hash_map | std::hash | 0.19 | 1.27 MiB | 365 | | ska::flat_hash_map | ClickHouse hash | 0.26 | 1.33 MiB | 366 | | ska::flat_hash_map | absl::Hash | 0.27 | 1.27 MiB | 367 | | ska::bytell_hash_map | std::hash | 0.23 | 800.00 KiB | 368 | | ska::bytell_hash_map | ClickHouse hash | 0.27 | 800.00 KiB | 369 | | ska::bytell_hash_map | absl::Hash | 0.30 | 800.00 KiB | 370 | | std::unordered_map | std::hash | 0.56 | 620.00 KiB | 371 | | std::unordered_map | ClickHouse hash | 1.24 | 884.00 KiB | 372 | | std::unordered_map | absl::Hash | 1.17 | 884.00 KiB | 373 | +------------------------------+-----------------+---------------+--------------+ 374 | 375 | File: data/CounterID.bin 376 | Key type: Int32 377 | Keys size: 99997497 378 | Unique keys size: 6506 379 | +------------------------------+-----------------+---------------+--------------+ 380 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 381 | +------------------------------+-----------------+---------------+--------------+ 382 | | ClickHouse HashMap | std::hash | 0.20 | 532.00 KiB | 383 | | ClickHouse HashMap | ClickHouse hash | 0.21 | 532.00 KiB | 384 | | ClickHouse HashMap | absl::Hash | 0.22 | 532.00 KiB | 385 | | absl::flat_hash_map | std::hash | 1.60 | 664.00 KiB | 386 | | absl::flat_hash_map | ClickHouse hash | 0.33 | 724.00 KiB | 387 | | absl::flat_hash_map | absl::Hash | 0.33 | 728.00 KiB | 388 | | google::dense_hash_map | std::hash | 0.28 | 780.00 KiB | 389 | | google::dense_hash_map | ClickHouse hash | 0.35 | 780.00 KiB | 390 | | google::dense_hash_map | absl::Hash | 0.34 | 780.00 KiB | 391 | | tsl::hopscotch_map | std::hash | 0.31 | 720.00 KiB | 392 | | tsl::hopscotch_map | ClickHouse hash | 0.49 | 720.00 KiB | 393 | | tsl::hopscotch_map | absl::Hash | 0.51 | 784.00 KiB | 394 | | ankerl::unordered_dense::map | std::hash | 0.44 | 620.00 KiB | 395 | | ankerl::unordered_dense::map | ClickHouse hash | 0.55 | 620.00 KiB | 396 | | ankerl::unordered_dense::map | absl::Hash | 0.55 | 620.00 KiB | 397 | | ska::flat_hash_map | std::hash | 0.20 | 912.00 KiB | 398 | | ska::flat_hash_map | ClickHouse hash | 0.21 | 976.00 KiB | 399 | | ska::flat_hash_map | absl::Hash | 0.22 | 912.00 KiB | 400 | | ska::bytell_hash_map | std::hash | 0.28 | 664.00 KiB | 401 | | ska::bytell_hash_map | ClickHouse hash | 0.34 | 664.00 KiB | 402 | | ska::bytell_hash_map | absl::Hash | 0.39 | 664.00 KiB | 403 | | std::unordered_map | std::hash | 0.30 | 620.00 KiB | 404 | | std::unordered_map | ClickHouse hash | 0.88 | 620.00 KiB | 405 | | std::unordered_map | absl::Hash | 0.78 | 620.00 KiB | 406 | +------------------------------+-----------------+---------------+--------------+ 407 | 408 | File: data/TraficSourceID.bin 409 | Key type: Int16 410 | Keys size: 99997497 411 | Unique keys size: 10 412 | +------------------------------+-----------------+---------------+--------------+ 413 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 414 | +------------------------------+-----------------+---------------+--------------+ 415 | | ClickHouse HashMap | std::hash | 0.24 | 4.00 KiB | 416 | | ClickHouse HashMap | ClickHouse hash | 0.27 | 4.00 KiB | 417 | | ClickHouse HashMap | absl::Hash | 0.27 | 4.00 KiB | 418 | | absl::flat_hash_map | std::hash | 0.26 | 4.00 KiB | 419 | | absl::flat_hash_map | ClickHouse hash | 0.31 | 4.00 KiB | 420 | | absl::flat_hash_map | absl::Hash | 0.33 | 4.00 KiB | 421 | | google::dense_hash_map | std::hash | 0.24 | 4.00 KiB | 422 | | google::dense_hash_map | ClickHouse hash | 0.51 | 4.00 KiB | 423 | | google::dense_hash_map | absl::Hash | 0.32 | 4.00 KiB | 424 | | tsl::hopscotch_map | std::hash | 0.16 | 4.00 KiB | 425 | | tsl::hopscotch_map | ClickHouse hash | 0.49 | 4.00 KiB | 426 | | tsl::hopscotch_map | absl::Hash | 0.58 | 4.00 KiB | 427 | | ankerl::unordered_dense::map | std::hash | 0.38 | 4.00 KiB | 428 | | ankerl::unordered_dense::map | ClickHouse hash | 0.71 | 4.00 KiB | 429 | | ankerl::unordered_dense::map | absl::Hash | 0.90 | 4.00 KiB | 430 | | ska::flat_hash_map | std::hash | 0.15 | 4.00 KiB | 431 | | ska::flat_hash_map | ClickHouse hash | 0.17 | 4.00 KiB | 432 | | ska::flat_hash_map | absl::Hash | 0.19 | 4.00 KiB | 433 | | ska::bytell_hash_map | std::hash | 0.21 | 4.00 KiB | 434 | | ska::bytell_hash_map | ClickHouse hash | 0.28 | 4.00 KiB | 435 | | ska::bytell_hash_map | absl::Hash | 0.41 | 4.00 KiB | 436 | | std::unordered_map | std::hash | 1.23 | 4.00 KiB | 437 | | std::unordered_map | ClickHouse hash | 1.55 | 4.00 KiB | 438 | | std::unordered_map | absl::Hash | 1.23 | 4.00 KiB | 439 | +------------------------------+-----------------+---------------+--------------+ 440 | 441 | File: data/AdvEngineID.bin 442 | Key type: Int16 443 | Keys size: 99997497 444 | Unique keys size: 19 445 | +------------------------------+-----------------+---------------+--------------+ 446 | | Hash Table | Hash Function | Elapsed (sec) | Memory Usage | 447 | +------------------------------+-----------------+---------------+--------------+ 448 | | ClickHouse HashMap | std::hash | 0.19 | 4.00 KiB | 449 | | ClickHouse HashMap | ClickHouse hash | 0.19 | 4.00 KiB | 450 | | ClickHouse HashMap | absl::Hash | 0.19 | 4.00 KiB | 451 | | absl::flat_hash_map | std::hash | 0.24 | 4.00 KiB | 452 | | absl::flat_hash_map | ClickHouse hash | 0.31 | 4.00 KiB | 453 | | absl::flat_hash_map | absl::Hash | 0.31 | 4.00 KiB | 454 | | google::dense_hash_map | std::hash | 0.24 | 4.00 KiB | 455 | | google::dense_hash_map | ClickHouse hash | 0.32 | 4.00 KiB | 456 | | google::dense_hash_map | absl::Hash | 0.32 | 4.00 KiB | 457 | | tsl::hopscotch_map | std::hash | 0.19 | 4.00 KiB | 458 | | tsl::hopscotch_map | ClickHouse hash | 0.31 | 4.00 KiB | 459 | | tsl::hopscotch_map | absl::Hash | 0.31 | 4.00 KiB | 460 | | ankerl::unordered_dense::map | std::hash | 0.40 | 4.00 KiB | 461 | | ankerl::unordered_dense::map | ClickHouse hash | 0.58 | 4.00 KiB | 462 | | ankerl::unordered_dense::map | absl::Hash | 0.54 | 4.00 KiB | 463 | | ska::flat_hash_map | std::hash | 0.19 | 4.00 KiB | 464 | | ska::flat_hash_map | ClickHouse hash | 0.19 | 4.00 KiB | 465 | | ska::flat_hash_map | absl::Hash | 0.21 | 4.00 KiB | 466 | | ska::bytell_hash_map | std::hash | 0.21 | 4.00 KiB | 467 | | ska::bytell_hash_map | ClickHouse hash | 0.26 | 4.00 KiB | 468 | | ska::bytell_hash_map | absl::Hash | 0.28 | 4.00 KiB | 469 | | std::unordered_map | std::hash | 0.36 | 4.00 KiB | 470 | | std::unordered_map | ClickHouse hash | 0.97 | 4.00 KiB | 471 | | std::unordered_map | absl::Hash | 0.87 | 4.00 KiB | 472 | +------------------------------+-----------------+---------------+--------------+ 473 | ``` 474 | -------------------------------------------------------------------------------- /contrib/ClickHouseHashTable/HashTable.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "types.h" 16 | 17 | // #include 18 | 19 | // #include 20 | // #include 21 | // #include 22 | // #include 23 | 24 | // #include 25 | // #include 26 | // #include 27 | // #include 28 | // #include 29 | 30 | #define ALWAYS_INLINE __attribute__((__always_inline__)) 31 | // #define DBMS_HASH_MAP_COUNT_COLLISIONS 32 | 33 | // #define ALWAYS_INLINE 34 | 35 | #if !defined(likely) 36 | # define likely(x) (__builtin_expect(!!(x), 1)) 37 | #endif 38 | #if !defined(unlikely) 39 | # define unlikely(x) (__builtin_expect(!!(x), 0)) 40 | #endif 41 | 42 | /** 43 | * Returns the key. Can return the temporary key initially. 44 | * After the call to keyHolderPersistKey(), must return the persistent key. 45 | */ 46 | template 47 | inline Key & ALWAYS_INLINE keyHolderGetKey(Key && key) { return key; } 48 | 49 | /** 50 | * Make the key persistent. keyHolderGetKey() must return the persistent key 51 | * after this call. 52 | */ 53 | template 54 | inline void ALWAYS_INLINE keyHolderPersistKey(Key &&) {} 55 | 56 | /** 57 | * Discard the key. Calling keyHolderGetKey() is ill-defined after this. 58 | */ 59 | template 60 | inline void ALWAYS_INLINE keyHolderDiscardKey(Key &&) {} 61 | 62 | 63 | template 64 | inline bool mulOverflow(T x, T y, T & res) 65 | { 66 | return __builtin_mul_overflow(x, y, &res); 67 | } 68 | 69 | template 70 | inline bool mulOverflow(T x, U y, R & res) 71 | { 72 | return __builtin_mul_overflow(x, y, &res); 73 | } 74 | 75 | template <> 76 | inline bool mulOverflow(int x, int y, int & res) 77 | { 78 | return __builtin_smul_overflow(x, y, &res); 79 | } 80 | 81 | template <> 82 | inline bool mulOverflow(long x, long y, long & res) 83 | { 84 | return __builtin_smull_overflow(x, y, &res); 85 | } 86 | 87 | template <> 88 | inline bool mulOverflow(long long x, long long y, long long & res) 89 | { 90 | return __builtin_smulll_overflow(x, y, &res); 91 | } 92 | 93 | #ifdef DBMS_HASH_MAP_DEBUG_RESIZES 94 | #include 95 | #include 96 | #include 97 | #endif 98 | 99 | /** NOTE HashTable could only be used for memmoveable (position independent) types. 100 | * Example: std::string is not position independent in libstdc++ with C++11 ABI or in libc++. 101 | * Also, key in hash table must be of type, that zero bytes is compared equals to zero key. 102 | */ 103 | 104 | 105 | namespace DB 106 | { 107 | namespace ErrorCodes 108 | { 109 | extern const int LOGICAL_ERROR; 110 | extern const int NO_AVAILABLE_DATA; 111 | extern const int CANNOT_ALLOCATE_MEMORY; 112 | extern const int TOO_LARGE_ARRAY_SIZE; 113 | } 114 | } 115 | 116 | 117 | /** The state of the hash table that affects the properties of its cells. 118 | * Used as a template parameter. 119 | * For example, there is an implementation of an instantly clearable hash table - ClearableHashMap. 120 | * For it, each cell holds the version number, and in the hash table itself is the current version. 121 | * When clearing, the current version simply increases; All cells with a mismatching version are considered empty. 122 | * Another example: for an approximate calculation of the number of unique visitors, there is a hash table for UniquesHashSet. 123 | * It has the concept of "degree". At each overflow, cells with keys that do not divide by the corresponding power of the two are deleted. 124 | */ 125 | struct HashTableNoState 126 | { 127 | /// Serialization, in binary and text form. 128 | // void write(DB::WriteBuffer &) const {} 129 | // void writeText(DB::WriteBuffer &) const {} 130 | 131 | /// Deserialization, in binary and text form. 132 | // void read(DB::ReadBuffer &) {} 133 | // void readText(DB::ReadBuffer &) {} 134 | }; 135 | 136 | 137 | /// These functions can be overloaded for custom types. 138 | namespace ZeroTraits 139 | { 140 | 141 | template 142 | bool check(const T x) { return x == T{}; } 143 | 144 | template 145 | void set(T & x) { x = T{}; } 146 | 147 | } 148 | 149 | 150 | /** Numbers are compared bitwise. 151 | * Complex types are compared by operator== as usual (this is important if there are gaps). 152 | * 153 | * This is needed if you use floats as keys. They are compared by bit equality. 154 | * Otherwise the invariants in hash table probing do not met when NaNs are present. 155 | */ 156 | template 157 | inline bool bitEquals(T && a, T && b) 158 | { 159 | using RealT = std::decay_t; 160 | 161 | if constexpr (std::is_floating_point_v) 162 | return 0 == memcmp(&a, &b, sizeof(RealT)); /// Note that memcmp with constant size is compiler builtin. 163 | else 164 | return a == b; 165 | } 166 | 167 | 168 | /** 169 | * getKey/Mapped -- methods to get key/"mapped" values from the LookupResult returned by find() and 170 | * emplace() methods of HashTable. Must not be called for a null LookupResult. 171 | * 172 | * We don't use iterators for lookup result. Instead, LookupResult is a pointer of some kind. There 173 | * are methods getKey/Mapped, that return references or values to key/"mapped" values. 174 | * 175 | * Different hash table implementations support this interface to a varying degree: 176 | * 177 | * 1) Hash tables that store neither the key in its original form, nor a "mapped" value: 178 | * FixedHashTable or StringHashTable. Neither GetKey nor GetMapped are supported, the only valid 179 | * operation is checking LookupResult for null. 180 | * 181 | * 2) Hash maps that do not store the key, e.g. FixedHashMap or StringHashMap. Only GetMapped is 182 | * supported. 183 | * 184 | * 3) Hash tables that store the key and do not have a "mapped" value, e.g. the normal HashTable. 185 | * GetKey returns the key, and GetMapped returns a zero void pointer. This simplifies generic 186 | * code that works with mapped values: it can overload on the return type of GetMapped(), and 187 | * doesn't need other parameters. One example is Cell::setMapped() function. 188 | * 189 | * 4) Hash tables that store both the key and the "mapped" value, e.g. HashMap. Both GetKey and 190 | * GetMapped are supported. 191 | * 192 | * The implementation side goes as follows: 193 | * 194 | * for (1), LookupResult->getKey = const VoidKey, LookupResult->getMapped = VoidMapped; 195 | * 196 | * for (2), LookupResult->getKey = const VoidKey, LookupResult->getMapped = Mapped &; 197 | * 198 | * for (3) and (4), LookupResult->getKey = const Key [&], LookupResult->getMapped = Mapped &; 199 | * VoidKey and VoidMapped may have specialized function overloads for generic code. 200 | */ 201 | 202 | struct VoidKey {}; 203 | struct VoidMapped 204 | { 205 | template 206 | auto & operator=(const T &) 207 | { 208 | return *this; 209 | } 210 | }; 211 | 212 | /** Compile-time interface for cell of the hash table. 213 | * Different cell types are used to implement different hash tables. 214 | * The cell must contain a key. 215 | * It can also contain a value and arbitrary additional data 216 | * (example: the stored hash value; version number for ClearableHashMap). 217 | */ 218 | template 219 | struct HashTableCell 220 | { 221 | using State = TState; 222 | 223 | using key_type = Key; 224 | using value_type = Key; 225 | using mapped_type = VoidMapped; 226 | 227 | Key key; 228 | 229 | HashTableCell() {} /// NOLINT 230 | 231 | /// Create a cell with the given key / key and value. 232 | HashTableCell(const Key & key_, const State &) : key(key_) {} 233 | 234 | /// Get the key (externally). 235 | const Key & getKey() const { return key; } 236 | VoidMapped getMapped() const { return {}; } 237 | const value_type & getValue() const { return key; } 238 | 239 | /// Get the key (internally). 240 | static const Key & getKey(const value_type & value) { return value; } 241 | 242 | /// Are the keys at the cells equal? 243 | bool keyEquals(const Key & key_) const { return bitEquals(key, key_); } 244 | bool keyEquals(const Key & key_, size_t /*hash_*/) const { return bitEquals(key, key_); } 245 | bool keyEquals(const Key & key_, size_t /*hash_*/, const State & /*state*/) const { return bitEquals(key, key_); } 246 | 247 | /// If the cell can remember the value of the hash function, then remember it. 248 | void setHash(size_t /*hash_value*/) {} 249 | 250 | /// If the cell can store the hash value in itself, then return the stored value. 251 | /// It must be at least once calculated before. 252 | /// If storing the hash value is not provided, then just compute the hash. 253 | size_t getHash(const Hash & hash) const { return hash(key); } 254 | 255 | /// Whether the key is zero. In the main buffer, cells with a zero key are considered empty. 256 | /// If zero keys can be inserted into the table, then the cell for the zero key is stored separately, not in the main buffer. 257 | /// Zero keys must be such that the zeroed-down piece of memory is a zero key. 258 | bool isZero(const State & state) const { return isZero(key, state); } 259 | static bool isZero(const Key & key, const State & /*state*/) { return ZeroTraits::check(key); } 260 | 261 | /// Set the key value to zero. 262 | void setZero() { ZeroTraits::set(key); } 263 | 264 | /// Do the hash table need to store the zero key separately (that is, can a zero key be inserted into the hash table). 265 | static constexpr bool need_zero_value_storage = true; 266 | 267 | /// Set the mapped value, if any (for HashMap), to the corresponding `value`. 268 | void setMapped(const value_type & /*value*/) {} 269 | 270 | /// Serialization, in binary and text form. 271 | // void write(DB::WriteBuffer & wb) const { DB::writeBinary(key, wb); } 272 | // void writeText(DB::WriteBuffer & wb) const { DB::writeDoubleQuoted(key, wb); } 273 | 274 | /// Deserialization, in binary and text form. 275 | // void read(DB::ReadBuffer & rb) { DB::readBinary(key, rb); } 276 | // void readText(DB::ReadBuffer & rb) { DB::readDoubleQuoted(key, rb); } 277 | 278 | /// When cell pointer is moved during erase, reinsert or resize operations 279 | 280 | static constexpr bool need_to_notify_cell_during_move = false; 281 | 282 | static void move(HashTableCell * /* old_location */, HashTableCell * /* new_location */) {} 283 | 284 | }; 285 | 286 | /** Determines the size of the hash table, and when and how much it should be resized. 287 | * Has very small state (one UInt8) and useful for Set-s allocated in automatic memory (see uniqExact as an example). 288 | */ 289 | template 290 | struct HashTableGrower 291 | { 292 | /// The state of this structure is enough to get the buffer size of the hash table. 293 | 294 | UInt8 size_degree = initial_size_degree; 295 | static constexpr auto initial_count = 1ULL << initial_size_degree; 296 | 297 | /// If collision resolution chains are contiguous, we can implement erase operation by moving the elements. 298 | static constexpr auto performs_linear_probing_with_single_step = true; 299 | 300 | static constexpr size_t max_size_degree = 23; 301 | 302 | /// The size of the hash table in the cells. 303 | size_t bufSize() const { return 1ULL << size_degree; } 304 | 305 | size_t maxFill() const { return 1ULL << (size_degree - 1); } 306 | size_t mask() const { return bufSize() - 1; } 307 | 308 | /// From the hash value, get the cell number in the hash table. 309 | size_t place(size_t x) const { return x * mask(); } 310 | 311 | /// The next cell in the collision resolution chain. 312 | size_t next(size_t pos) const { ++pos; return pos & mask(); } 313 | 314 | /// Whether the hash table is sufficiently full. You need to increase the size of the hash table, or remove something unnecessary from it. 315 | bool overflow(size_t elems) const { return elems > maxFill(); } 316 | 317 | /// Increase the size of the hash table. 318 | void increaseSize() 319 | { 320 | size_degree += size_degree >= max_size_degree ? 1 : 2; 321 | } 322 | 323 | /// Set the buffer size by the number of elements in the hash table. Used when deserializing a hash table. 324 | void set(size_t num_elems) 325 | { 326 | if (num_elems <= 1) 327 | size_degree = initial_size_degree; 328 | else if (initial_size_degree > static_cast(log2(num_elems - 1)) + 2) 329 | size_degree = initial_size_degree; 330 | else 331 | size_degree = static_cast(log2(num_elems - 1)) + 2; 332 | } 333 | 334 | void setBufSize(size_t buf_size_) 335 | { 336 | size_degree = static_cast(log2(buf_size_ - 1) + 1); 337 | } 338 | }; 339 | 340 | /** Determines the size of the hash table, and when and how much it should be resized. 341 | * This structure is aligned to cache line boundary and also occupies it all. 342 | * Precalculates some values to speed up lookups and insertion into the HashTable (and thus has bigger memory footprint than HashTableGrower). 343 | * This grower assume 0.5 load factor 344 | */ 345 | template 346 | class alignas(64) HashTableGrowerWithPrecalculation 347 | { 348 | /// The state of this structure is enough to get the buffer size of the hash table. 349 | 350 | UInt8 size_degree = initial_size_degree; 351 | size_t precalculated_mask = (1ULL << initial_size_degree) - 1; 352 | size_t precalculated_max_fill = 1ULL << (initial_size_degree - 1); 353 | static constexpr size_t max_size_degree = 23; 354 | 355 | public: 356 | UInt8 sizeDegree() const { return size_degree; } 357 | 358 | void increaseSizeDegree(UInt8 delta) 359 | { 360 | size_degree += delta; 361 | precalculated_mask = (1ULL << size_degree) - 1; 362 | precalculated_max_fill = 1ULL << (size_degree - 1); 363 | } 364 | 365 | static constexpr auto initial_count = 1ULL << initial_size_degree; 366 | 367 | /// If collision resolution chains are contiguous, we can implement erase operation by moving the elements. 368 | static constexpr auto performs_linear_probing_with_single_step = true; 369 | 370 | /// The size of the hash table in the cells. 371 | size_t bufSize() const { return 1ULL << size_degree; } 372 | 373 | /// From the hash value, get the cell number in the hash table. 374 | size_t place(size_t x) const { return x & precalculated_mask; } 375 | 376 | /// The next cell in the collision resolution chain. 377 | size_t next(size_t pos) const { return (pos + 1) & precalculated_mask; } 378 | 379 | /// Whether the hash table is sufficiently full. You need to increase the size of the hash table, or remove something unnecessary from it. 380 | bool overflow(size_t elems) const { return elems > precalculated_max_fill; } 381 | 382 | /// Increase the size of the hash table. 383 | void increaseSize() { increaseSizeDegree(size_degree >= max_size_degree ? 1 : 2); } 384 | 385 | /// Set the buffer size by the number of elements in the hash table. Used when deserializing a hash table. 386 | void set(size_t num_elems) 387 | { 388 | if (num_elems <= 1) 389 | size_degree = initial_size_degree; 390 | else if (initial_size_degree > static_cast(log2(num_elems - 1)) + 2) 391 | size_degree = initial_size_degree; 392 | else 393 | size_degree = static_cast(log2(num_elems - 1)) + 2; 394 | increaseSizeDegree(0); 395 | } 396 | 397 | void setBufSize(size_t buf_size_) 398 | { 399 | size_degree = static_cast(log2(buf_size_ - 1) + 1); 400 | increaseSizeDegree(0); 401 | } 402 | }; 403 | 404 | static_assert(sizeof(HashTableGrowerWithPrecalculation<>) == 64); 405 | 406 | /** When used as a Grower, it turns a hash table into something like a lookup table. 407 | * It remains non-optimal - the cells store the keys. 408 | * Also, the compiler can not completely remove the code of passing through the collision resolution chain, although it is not needed. 409 | * NOTE: Better to use FixedHashTable instead. 410 | */ 411 | template 412 | struct HashTableFixedGrower 413 | { 414 | static constexpr auto initial_count = 1ULL << key_bits; 415 | 416 | static constexpr auto performs_linear_probing_with_single_step = true; 417 | 418 | size_t bufSize() const { return 1ULL << key_bits; } 419 | size_t place(size_t x) const { return x; } 420 | /// You could write UNREACHABLE(), but the compiler does not optimize everything, and it turns out less efficiently. 421 | size_t next(size_t pos) const { return pos + 1; } 422 | bool overflow(size_t /*elems*/) const { return false; } 423 | 424 | void increaseSize() { } 425 | void set(size_t /*num_elems*/) {} 426 | void setBufSize(size_t /*buf_size_*/) {} 427 | }; 428 | 429 | 430 | /** If you want to store the zero key separately - a place to store it. */ 431 | template 432 | struct ZeroValueStorage; 433 | 434 | template 435 | struct ZeroValueStorage 436 | { 437 | private: 438 | bool has_zero = false; 439 | std::aligned_storage_t zero_value_storage; /// Storage of element with zero key. 440 | 441 | public: 442 | bool hasZero() const { return has_zero; } 443 | 444 | void setHasZero() 445 | { 446 | has_zero = true; 447 | new (zeroValue()) Cell(); 448 | } 449 | 450 | void clearHasZero() 451 | { 452 | has_zero = false; 453 | zeroValue()->~Cell(); 454 | } 455 | 456 | void clearHasZeroFlag() 457 | { 458 | has_zero = false; 459 | } 460 | 461 | Cell * zeroValue() { return std::launder(reinterpret_cast(&zero_value_storage)); } 462 | const Cell * zeroValue() const { return std::launder(reinterpret_cast(&zero_value_storage)); } 463 | }; 464 | 465 | template 466 | struct ZeroValueStorage 467 | { 468 | bool hasZero() const { return false; } 469 | void setHasZero() { throw std::logic_error("HashTable: logical error"); } 470 | void clearHasZero() {} 471 | void clearHasZeroFlag() {} 472 | 473 | Cell * zeroValue() { return nullptr; } 474 | const Cell * zeroValue() const { return nullptr; } 475 | }; 476 | 477 | 478 | template 479 | struct AllocatorBufferDeleter; 480 | 481 | template 482 | struct AllocatorBufferDeleter 483 | { 484 | AllocatorBufferDeleter(Allocator &, size_t) {} 485 | 486 | void operator()(Cell *) const {} 487 | 488 | }; 489 | 490 | template 491 | struct AllocatorBufferDeleter 492 | { 493 | AllocatorBufferDeleter(Allocator & allocator_, size_t size_) 494 | : allocator(allocator_) 495 | , size(size_) {} 496 | 497 | void operator()(Cell * buffer) const { allocator.free(buffer, size); } 498 | 499 | Allocator & allocator; 500 | size_t size; 501 | }; 502 | 503 | 504 | // The HashTable 505 | template 506 | class HashTable : protected Hash, 507 | protected Allocator, 508 | protected Cell::State, 509 | public ZeroValueStorage /// empty base optimization 510 | { 511 | public: 512 | // If we use an allocator with inline memory, check that the initial 513 | // size of the hash table is in sync with the amount of this memory. 514 | static constexpr size_t initial_buffer_bytes 515 | = Grower::initial_count * sizeof(Cell); 516 | // static_assert(allocatorInitialBytes == 0 517 | // || allocatorInitialBytes == initial_buffer_bytes); 518 | 519 | protected: 520 | friend class const_iterator; 521 | friend class iterator; 522 | friend class Reader; 523 | 524 | template 525 | friend class TwoLevelHashTable; 526 | 527 | template 528 | friend class TwoLevelStringHashTable; 529 | 530 | template 531 | friend class StringHashTable; 532 | 533 | using HashValue = size_t; 534 | using Self = HashTable; 535 | 536 | size_t m_size = 0; /// Amount of elements 537 | Cell * buf; /// A piece of memory for all elements except the element with zero key. 538 | Grower grower; 539 | 540 | #ifdef DBMS_HASH_MAP_COUNT_COLLISIONS 541 | mutable size_t collisions = 0; 542 | mutable size_t max_chain_length = 0; 543 | #endif 544 | 545 | /// Find a cell with the same key or an empty cell, starting from the specified position and further along the collision resolution chain. 546 | size_t ALWAYS_INLINE findCell(const Key & x, size_t hash_value, size_t place_value) const 547 | { 548 | // size_t chain_length = 0; 549 | while (!buf[place_value].isZero(*this) && !buf[place_value].keyEquals(x, hash_value, *this)) 550 | { 551 | // ++chain_length; 552 | place_value = grower.next(place_value); 553 | } 554 | 555 | // #ifdef DBMS_HASH_MAP_COUNT_COLLISIONS 556 | // collisions += chain_length; 557 | // max_chain_length = std::max(max_chain_length, chain_length); 558 | // #endif 559 | 560 | return place_value; 561 | } 562 | 563 | /// Find a cell with the same key or an empty cell, starting from the specified position and further along the collision resolution chain. 564 | std::pair ALWAYS_INLINE findCellUpd(const Key & x, size_t hash_value, size_t place_value) const 565 | { 566 | while (true) 567 | { 568 | if (buf[place_value].isZero(*this)) 569 | { 570 | return {place_value, true}; 571 | } 572 | 573 | if (buf[place_value].keyEquals(x, hash_value, *this)) 574 | return {place_value, false}; 575 | 576 | place_value = grower.next(place_value); 577 | } 578 | 579 | __builtin_unreachable(); 580 | } 581 | 582 | 583 | /// Find an empty cell, starting with the specified position and further along the collision resolution chain. 584 | size_t ALWAYS_INLINE findEmptyCell(size_t place_value) const 585 | { 586 | while (!buf[place_value].isZero(*this)) 587 | { 588 | place_value = grower.next(place_value); 589 | #ifdef DBMS_HASH_MAP_COUNT_COLLISIONS 590 | ++collisions; 591 | #endif 592 | } 593 | 594 | return place_value; 595 | } 596 | 597 | static size_t allocCheckOverflow(size_t buffer_size) 598 | { 599 | size_t size = 0; 600 | if (mulOverflow(buffer_size, sizeof(Cell), size)) 601 | throw std::runtime_error("Integer overflow trying to allocate memory for HashTable"); 602 | 603 | return size; 604 | } 605 | 606 | void alloc(const Grower & new_grower) 607 | { 608 | buf = reinterpret_cast(Allocator::alloc(allocCheckOverflow(new_grower.bufSize()))); 609 | grower = new_grower; 610 | } 611 | 612 | void free() 613 | { 614 | if (buf) 615 | { 616 | Allocator::free(buf, getBufferSizeInBytes()); 617 | buf = nullptr; 618 | } 619 | } 620 | 621 | /// Increase the size of the buffer. 622 | void resize(size_t for_num_elems = 0, size_t for_buf_size = 0) 623 | { 624 | #ifdef DBMS_HASH_MAP_DEBUG_RESIZES 625 | Stopwatch watch; 626 | #endif 627 | 628 | size_t old_size = grower.bufSize(); 629 | 630 | /** In case of exception for the object to remain in the correct state, 631 | * changing the variable `grower` (which determines the buffer size of the hash table) 632 | * is postponed for a moment after a real buffer change. 633 | * The temporary variable `new_grower` is used to determine the new size. 634 | */ 635 | Grower new_grower = grower; 636 | 637 | if (for_num_elems) 638 | { 639 | new_grower.set(for_num_elems); 640 | if (new_grower.bufSize() <= old_size) 641 | return; 642 | } 643 | else if (for_buf_size) 644 | { 645 | new_grower.setBufSize(for_buf_size); 646 | if (new_grower.bufSize() <= old_size) 647 | return; 648 | } 649 | else 650 | new_grower.increaseSize(); 651 | 652 | /// Expand the space. 653 | 654 | size_t old_buffer_size = getBufferSizeInBytes(); 655 | 656 | /** If cell required to be notified during move we need to temporary keep old buffer 657 | * because realloc does not quarantee for reallocated buffer to have same base address 658 | */ 659 | using Deleter = AllocatorBufferDeleter; 660 | Deleter buffer_deleter(*this, old_buffer_size); 661 | std::unique_ptr old_buffer(buf, buffer_deleter); 662 | 663 | if constexpr (Cell::need_to_notify_cell_during_move) 664 | { 665 | buf = reinterpret_cast(Allocator::alloc(allocCheckOverflow(new_grower.bufSize()))); 666 | memcpy(reinterpret_cast(buf), reinterpret_cast(old_buffer.get()), old_buffer_size); 667 | } 668 | else 669 | buf = reinterpret_cast(Allocator::realloc(buf, old_buffer_size, allocCheckOverflow(new_grower.bufSize()))); 670 | 671 | grower = new_grower; 672 | 673 | /** Now some items may need to be moved to a new location. 674 | * The element can stay in place, or move to a new location "on the right", 675 | * or move to the left of the collision resolution chain, because the elements to the left of it have been moved to the new "right" location. 676 | */ 677 | size_t i = 0; 678 | for (; i < old_size; ++i) 679 | if (!buf[i].isZero(*this)) 680 | { 681 | size_t updated_place_value = reinsert(buf[i], buf[i].getHash(*this)); 682 | 683 | if constexpr (Cell::need_to_notify_cell_during_move) 684 | Cell::move(&(old_buffer.get())[i], &buf[updated_place_value]); 685 | } 686 | 687 | /** There is also a special case: 688 | * if the element was to be at the end of the old buffer, [ x] 689 | * but is at the beginning because of the collision resolution chain, [o x] 690 | * then after resizing, it will first be out of place again, [ xo ] 691 | * and in order to transfer it where necessary, 692 | * after transferring all the elements from the old halves you need to [ o x ] 693 | * process tail from the collision resolution chain immediately after it [ o x ] 694 | */ 695 | size_t new_size = grower.bufSize(); 696 | for (; i < new_size && !buf[i].isZero(*this); ++i) 697 | { 698 | size_t updated_place_value = reinsert(buf[i], buf[i].getHash(*this)); 699 | 700 | if constexpr (Cell::need_to_notify_cell_during_move) 701 | if (&buf[i] != &buf[updated_place_value]) 702 | Cell::move(&buf[i], &buf[updated_place_value]); 703 | } 704 | 705 | #ifdef DBMS_HASH_MAP_DEBUG_RESIZES 706 | watch.stop(); 707 | std::cerr << std::fixed << std::setprecision(3) 708 | << "Resize from " << old_size << " to " << grower.bufSize() << " took " << watch.elapsedSeconds() << " sec." 709 | << std::endl; 710 | #endif 711 | } 712 | 713 | 714 | /** Paste into the new buffer the value that was in the old buffer. 715 | * Used when increasing the buffer size. 716 | */ 717 | size_t reinsert(Cell & x, size_t hash_value) 718 | { 719 | size_t place_value = grower.place(hash_value); 720 | 721 | /// If the element is in its place. 722 | if (&x == &buf[place_value]) 723 | return place_value; 724 | 725 | /// Compute a new location, taking into account the collision resolution chain. 726 | place_value = findCell(Cell::getKey(x.getValue()), hash_value, place_value); 727 | 728 | /// If the item remains in its place in the old collision resolution chain. 729 | if (!buf[place_value].isZero(*this)) 730 | return place_value; 731 | 732 | // auto [cell_place_value, is_zero] = findCellUpd(Cell::getKey(x.getValue()), hash_value, place_value); 733 | // place_value = cell_place_value; 734 | // if (!is_zero) 735 | // return place_value; 736 | 737 | /// Copy to a new location and zero the old one. 738 | x.setHash(hash_value); 739 | memcpy(static_cast(&buf[place_value]), &x, sizeof(x)); 740 | x.setZero(); 741 | 742 | /// Then the elements that previously were in collision with this can move to the old place. 743 | return place_value; 744 | } 745 | 746 | 747 | void destroyElements() 748 | { 749 | if (!std::is_trivially_destructible_v) 750 | { 751 | for (iterator it = begin(), it_end = end(); it != it_end; ++it) 752 | { 753 | it.ptr->~Cell(); 754 | /// In case of poison_in_dtor=1 it will be poisoned, 755 | /// but it maybe used later, during iteration. 756 | /// 757 | /// NOTE, that technically this is UB [1], but OK for now. 758 | /// 759 | /// [1]: https://github.com/google/sanitizers/issues/854#issuecomment-329661378 760 | // __msan_unpoison(it.ptr, sizeof(*it.ptr)); 761 | } 762 | 763 | /// Everything had been destroyed in the loop above, reset the flag 764 | /// only, without calling destructor. 765 | this->clearHasZeroFlag(); 766 | } 767 | else 768 | { 769 | /// NOTE: it is OK to call dtor for trivially destructible type 770 | /// even the object hadn't been initialized, so no need to has 771 | /// hasZero() check. 772 | this->clearHasZero(); 773 | } 774 | } 775 | 776 | 777 | template 778 | class iterator_base /// NOLINT 779 | { 780 | using Container = std::conditional_t; 781 | using cell_type = std::conditional_t; 782 | 783 | Container * container; 784 | cell_type * ptr; 785 | 786 | friend class HashTable; 787 | 788 | public: 789 | iterator_base() {} /// NOLINT 790 | iterator_base(Container * container_, cell_type * ptr_) : container(container_), ptr(ptr_) {} 791 | 792 | bool operator== (const iterator_base & rhs) const { return ptr == rhs.ptr; } 793 | bool operator!= (const iterator_base & rhs) const { return ptr != rhs.ptr; } 794 | 795 | Derived & operator++() 796 | { 797 | /// If iterator was pointed to ZeroValueStorage, move it to the beginning of the main buffer. 798 | if (unlikely(ptr->isZero(*container))) 799 | ptr = container->buf; 800 | else 801 | ++ptr; 802 | 803 | /// Skip empty cells in the main buffer. 804 | auto * buf_end = container->buf + container->grower.bufSize(); 805 | while (ptr < buf_end && ptr->isZero(*container)) 806 | ++ptr; 807 | 808 | return static_cast(*this); 809 | } 810 | 811 | auto & operator* () const { return *ptr; } 812 | auto * operator->() const { return ptr; } 813 | 814 | auto getPtr() const { return ptr; } 815 | size_t getHash() const { return ptr->getHash(*container); } 816 | 817 | size_t getCollisionChainLength() const 818 | { 819 | return container->grower.place((ptr - container->buf) - container->grower.place(getHash())); 820 | } 821 | 822 | /** 823 | * A hack for HashedDictionary. 824 | * 825 | * The problem: std-like find() returns an iterator, which has to be 826 | * compared to end(). On the other hand, HashMap::find() returns 827 | * LookupResult, which is compared to nullptr. HashedDictionary has to 828 | * support both hash maps with the same code, hence the need for this 829 | * hack. 830 | * 831 | * The proper way would be to remove iterator interface from our 832 | * HashMap completely, change all its users to the existing internal 833 | * iteration interface, and redefine end() to return LookupResult for 834 | * compatibility with std find(). Unfortunately, now is not the time to 835 | * do this. 836 | */ 837 | operator Cell * () const { return nullptr; } /// NOLINT 838 | }; 839 | 840 | 841 | public: 842 | using key_type = Key; 843 | using grower_type = Grower; 844 | using mapped_type = typename Cell::mapped_type; 845 | using value_type = typename Cell::value_type; 846 | using cell_type = Cell; 847 | 848 | using LookupResult = Cell *; 849 | using ConstLookupResult = const Cell *; 850 | 851 | size_t hash(const Key & x) const { return Hash::operator()(x); } 852 | 853 | 854 | HashTable() 855 | { 856 | if (Cell::need_zero_value_storage) 857 | this->zeroValue()->setZero(); 858 | alloc(grower); 859 | } 860 | 861 | explicit HashTable(const Grower & grower_) 862 | : grower(grower_) 863 | { 864 | if (Cell::need_zero_value_storage) 865 | this->zeroValue()->setZero(); 866 | alloc(grower); 867 | } 868 | 869 | HashTable(const HashTable &) = delete; 870 | HashTable& operator=(const HashTable &) = delete; 871 | 872 | HashTable(size_t reserve_for_num_elements) /// NOLINT 873 | { 874 | if (Cell::need_zero_value_storage) 875 | this->zeroValue()->setZero(); 876 | grower.set(reserve_for_num_elements); 877 | alloc(grower); 878 | } 879 | 880 | HashTable(HashTable && rhs) noexcept 881 | : buf(nullptr) 882 | { 883 | *this = std::move(rhs); 884 | } 885 | 886 | ~HashTable() 887 | { 888 | destroyElements(); 889 | free(); 890 | #ifdef DBMS_HASH_MAP_COUNT_COLLISIONS 891 | std::cout << "Collisions " << collisions << " max chain length " << max_chain_length << '\n'; 892 | #endif 893 | } 894 | 895 | HashTable & operator=(HashTable && rhs) noexcept 896 | { 897 | destroyElements(); 898 | free(); 899 | 900 | std::swap(buf, rhs.buf); 901 | std::swap(m_size, rhs.m_size); 902 | std::swap(grower, rhs.grower); 903 | 904 | Hash::operator=(std::move(rhs)); ///NOLINT 905 | Allocator::operator=(std::move(rhs)); ///NOLINT 906 | Cell::State::operator=(std::move(rhs)); ///NOLINT 907 | ZeroValueStorage::operator=(std::move(rhs)); ///NOLINT 908 | 909 | return *this; 910 | } 911 | 912 | // class Reader final : private Cell::State 913 | // { 914 | // public: 915 | // explicit Reader(DB::ReadBuffer & in_) 916 | // : in(in_) 917 | // { 918 | // } 919 | 920 | // Reader(const Reader &) = delete; 921 | // Reader & operator=(const Reader &) = delete; 922 | 923 | // bool next() 924 | // { 925 | // if (!is_initialized) 926 | // { 927 | // Cell::State::read(in);d 928 | // DB::readVarUInt(size, in); 929 | // is_initialized = true; 930 | // } 931 | 932 | // if (read_count == size) 933 | // { 934 | // is_eof = true; 935 | // return false; 936 | // } 937 | 938 | // cell.read(in); 939 | // ++read_count; 940 | 941 | // return true; 942 | // } 943 | 944 | // inline const value_type & get() const 945 | // { 946 | // if (!is_initialized || is_eof) 947 | // throw DB::Exception(DB::ErrorCodes::NO_AVAILABLE_DATA, "No available data"); 948 | 949 | // return cell.getValue(); 950 | // } 951 | 952 | // private: 953 | // DB::ReadBuffer & in; 954 | // Cell cell; 955 | // size_t read_count = 0; 956 | // size_t size = 0; 957 | // bool is_eof = false; 958 | // bool is_initialized = false; 959 | // }; 960 | 961 | 962 | class iterator : public iterator_base /// NOLINT 963 | { 964 | public: 965 | using iterator_base::iterator_base; 966 | }; 967 | 968 | class const_iterator : public iterator_base /// NOLINT 969 | { 970 | public: 971 | using iterator_base::iterator_base; 972 | }; 973 | 974 | 975 | const_iterator begin() const 976 | { 977 | if (!buf) 978 | return end(); 979 | 980 | if (this->hasZero()) 981 | return iteratorToZero(); 982 | 983 | const Cell * ptr = buf; 984 | auto buf_end = buf + grower.bufSize(); 985 | while (ptr < buf_end && ptr->isZero(*this)) 986 | ++ptr; 987 | 988 | return const_iterator(this, ptr); 989 | } 990 | 991 | const_iterator cbegin() const { return begin(); } 992 | 993 | iterator begin() 994 | { 995 | if (!buf) 996 | return end(); 997 | 998 | if (this->hasZero()) 999 | return iteratorToZero(); 1000 | 1001 | Cell * ptr = buf; 1002 | auto * buf_end = buf + grower.bufSize(); 1003 | while (ptr < buf_end && ptr->isZero(*this)) 1004 | ++ptr; 1005 | 1006 | return iterator(this, ptr); 1007 | } 1008 | 1009 | const_iterator end() const 1010 | { 1011 | /// Avoid UBSan warning about adding zero to nullptr. It is valid in C++20 (and earlier) but not valid in C. 1012 | return const_iterator(this, buf ? buf + grower.bufSize() : buf); 1013 | } 1014 | 1015 | const_iterator cend() const 1016 | { 1017 | return end(); 1018 | } 1019 | 1020 | iterator end() 1021 | { 1022 | return iterator(this, buf ? buf + grower.bufSize() : buf); 1023 | } 1024 | 1025 | 1026 | protected: 1027 | const_iterator iteratorTo(const Cell * ptr) const { return const_iterator(this, ptr); } 1028 | iterator iteratorTo(Cell * ptr) { return iterator(this, ptr); } 1029 | const_iterator iteratorToZero() const { return iteratorTo(this->zeroValue()); } 1030 | iterator iteratorToZero() { return iteratorTo(this->zeroValue()); } 1031 | 1032 | 1033 | /// If the key is zero, insert it into a special place and return true. 1034 | /// We don't have to persist a zero key, because it's not actually inserted. 1035 | /// That's why we just take a Key by value, an not a key holder. 1036 | bool ALWAYS_INLINE emplaceIfZero(const Key & x, LookupResult & it, bool & inserted, size_t hash_value) 1037 | { 1038 | /// If it is claimed that the zero key can not be inserted into the table. 1039 | if constexpr (!Cell::need_zero_value_storage) 1040 | return false; 1041 | 1042 | if (unlikely(Cell::isZero(x, *this))) 1043 | { 1044 | it = this->zeroValue(); 1045 | 1046 | if (!this->hasZero()) 1047 | { 1048 | ++m_size; 1049 | this->setHasZero(); 1050 | this->zeroValue()->setHash(hash_value); 1051 | inserted = true; 1052 | } 1053 | else 1054 | inserted = false; 1055 | 1056 | return true; 1057 | } 1058 | 1059 | return false; 1060 | } 1061 | 1062 | template 1063 | void ALWAYS_INLINE emplaceNonZeroImpl(size_t place_value, KeyHolder && key_holder, 1064 | LookupResult & it, bool & inserted, size_t hash_value) 1065 | { 1066 | it = &buf[place_value]; 1067 | 1068 | if (!buf[place_value].isZero(*this)) 1069 | { 1070 | keyHolderDiscardKey(key_holder); 1071 | inserted = false; 1072 | return; 1073 | } 1074 | 1075 | keyHolderPersistKey(key_holder); 1076 | const auto & key = keyHolderGetKey(key_holder); 1077 | 1078 | new (&buf[place_value]) Cell(key, *this); 1079 | buf[place_value].setHash(hash_value); 1080 | inserted = true; 1081 | ++m_size; 1082 | 1083 | if (unlikely(grower.overflow(m_size))) 1084 | { 1085 | try 1086 | { 1087 | resize(); 1088 | } 1089 | catch (...) 1090 | { 1091 | /** If we have not resized successfully, then there will be problems. 1092 | * There remains a key, but uninitialized mapped-value, 1093 | * which, perhaps, can not even be called a destructor. 1094 | */ 1095 | --m_size; 1096 | buf[place_value].setZero(); 1097 | inserted = false; 1098 | throw; 1099 | } 1100 | 1101 | // The hash table was rehashed, so we have to re-find the key. 1102 | size_t new_place = findCell(key, hash_value, grower.place(hash_value)); 1103 | assert(!buf[new_place].isZero(*this)); 1104 | it = &buf[new_place]; 1105 | } 1106 | } 1107 | 1108 | /// Only for non-zero keys. Find the right place, insert the key there, if it does not already exist. Set iterator to the cell in output parameter. 1109 | template 1110 | void ALWAYS_INLINE emplaceNonZero(KeyHolder && key_holder, LookupResult & it, 1111 | bool & inserted, size_t hash_value) 1112 | { 1113 | const auto & key = keyHolderGetKey(key_holder); 1114 | size_t place_value = findCell(key, hash_value, grower.place(hash_value)); 1115 | // auto [place_value, is_zero] = findCellUpd(key, hash_value, grower.place(hash_value)); 1116 | // if (!is_zero) { 1117 | // inserted = false; 1118 | // return; 1119 | // } 1120 | 1121 | emplaceNonZeroImpl(place_value, key_holder, it, inserted, hash_value); 1122 | } 1123 | 1124 | void ALWAYS_INLINE prefetchByHash(size_t hash_key) const 1125 | { 1126 | const auto place = grower.place(hash_key); 1127 | __builtin_prefetch(&buf[place]); 1128 | } 1129 | 1130 | public: 1131 | void reserve(size_t num_elements) 1132 | { 1133 | resize(num_elements); 1134 | } 1135 | 1136 | /// Insert a value. In the case of any more complex values, it is better to use the `emplace` function. 1137 | std::pair ALWAYS_INLINE insert(const value_type & x) 1138 | { 1139 | std::pair res; 1140 | 1141 | size_t hash_value = hash(Cell::getKey(x)); 1142 | if (!emplaceIfZero(Cell::getKey(x), res.first, res.second, hash_value)) 1143 | { 1144 | emplaceNonZero(Cell::getKey(x), res.first, res.second, hash_value); 1145 | } 1146 | 1147 | if (res.second) 1148 | res.first->setMapped(x); 1149 | 1150 | return res; 1151 | } 1152 | 1153 | /// Reinsert node pointed to by iterator 1154 | void ALWAYS_INLINE reinsert(iterator & it, size_t hash_value) 1155 | { 1156 | size_t place_value = reinsert(*it.getPtr(), hash_value); 1157 | 1158 | if constexpr (Cell::need_to_notify_cell_during_move) 1159 | if (it.getPtr() != &buf[place_value]) 1160 | Cell::move(it.getPtr(), &buf[place_value]); 1161 | } 1162 | 1163 | template 1164 | void ALWAYS_INLINE prefetch(KeyHolder && key_holder) const 1165 | { 1166 | const auto & key = keyHolderGetKey(key_holder); 1167 | const auto key_hash = hash(key); 1168 | prefetchByHash(key_hash); 1169 | } 1170 | 1171 | /** Insert the key. 1172 | * Return values: 1173 | * 'it' -- a LookupResult pointing to the corresponding key/mapped pair. 1174 | * 'inserted' -- whether a new key was inserted. 1175 | * 1176 | * You have to make `placement new` of value if you inserted a new key, 1177 | * since when destroying a hash table, it will call the destructor! 1178 | * 1179 | * Example usage: 1180 | * 1181 | * Map::LookupResult it; 1182 | * bool inserted; 1183 | * map.emplace(key, it, inserted); 1184 | * if (inserted) 1185 | * new (&it->getMapped()) Mapped(value); 1186 | */ 1187 | template 1188 | void ALWAYS_INLINE emplace(KeyHolder && key_holder, LookupResult & it, bool & inserted) 1189 | { 1190 | const auto & key = keyHolderGetKey(key_holder); 1191 | emplace(key_holder, it, inserted, hash(key)); 1192 | } 1193 | 1194 | template 1195 | void ALWAYS_INLINE emplace(KeyHolder && key_holder, LookupResult & it, 1196 | bool & inserted, size_t hash_value) 1197 | { 1198 | const auto & key = keyHolderGetKey(key_holder); 1199 | if (!emplaceIfZero(key, it, inserted, hash_value)) 1200 | emplaceNonZero(key_holder, it, inserted, hash_value); 1201 | } 1202 | 1203 | /// Copy the cell from another hash table. It is assumed that the cell is not zero, and also that there was no such key in the table yet. 1204 | void ALWAYS_INLINE insertUniqueNonZero(const Cell * cell, size_t hash_value) 1205 | { 1206 | size_t place_value = findEmptyCell(grower.place(hash_value)); 1207 | 1208 | memcpy(static_cast(&buf[place_value]), cell, sizeof(*cell)); 1209 | ++m_size; 1210 | 1211 | if (unlikely(grower.overflow(m_size))) 1212 | resize(); 1213 | } 1214 | 1215 | LookupResult ALWAYS_INLINE find(const Key & x) 1216 | { 1217 | if (Cell::isZero(x, *this)) 1218 | return this->hasZero() ? this->zeroValue() : nullptr; 1219 | 1220 | size_t hash_value = hash(x); 1221 | size_t place_value = findCell(x, hash_value, grower.place(hash_value)); 1222 | return !buf[place_value].isZero(*this) ? &buf[place_value] : nullptr; 1223 | } 1224 | 1225 | ConstLookupResult ALWAYS_INLINE find(const Key & x) const 1226 | { 1227 | return const_cast *>(this)->find(x); 1228 | } 1229 | 1230 | LookupResult ALWAYS_INLINE find(const Key & x, size_t hash_value) 1231 | { 1232 | if (Cell::isZero(x, *this)) 1233 | return this->hasZero() ? this->zeroValue() : nullptr; 1234 | 1235 | size_t place_value = findCell(x, hash_value, grower.place(hash_value)); 1236 | return !buf[place_value].isZero(*this) ? &buf[place_value] : nullptr; 1237 | } 1238 | 1239 | ConstLookupResult ALWAYS_INLINE find(const Key & x, size_t hash_value) const 1240 | { 1241 | return const_cast *>(this)->find(x, hash_value); 1242 | } 1243 | 1244 | ALWAYS_INLINE bool erase(const Key & x) 1245 | requires Grower::performs_linear_probing_with_single_step 1246 | { 1247 | return erase(x, hash(x)); 1248 | } 1249 | 1250 | ALWAYS_INLINE bool erase(const Key & x, size_t hash_value) 1251 | requires Grower::performs_linear_probing_with_single_step 1252 | { 1253 | /** Deletion from open addressing hash table without tombstones 1254 | * 1255 | * https://en.wikipedia.org/wiki/Linear_probing 1256 | * https://en.wikipedia.org/wiki/Open_addressing 1257 | * Algorithm without recomputing hash but keep probes difference value (difference of natural cell position and inserted one) 1258 | * in cell https://arxiv.org/ftp/arxiv/papers/0909/0909.2547.pdf 1259 | * 1260 | * Currently we use algorithm with hash recomputing on each step from https://en.wikipedia.org/wiki/Open_addressing 1261 | */ 1262 | 1263 | if (Cell::isZero(x, *this)) 1264 | { 1265 | if (this->hasZero()) 1266 | { 1267 | --m_size; 1268 | this->clearHasZero(); 1269 | return true; 1270 | } 1271 | else 1272 | { 1273 | return false; 1274 | } 1275 | } 1276 | 1277 | size_t erased_key_position = findCell(x, hash_value, grower.place(hash_value)); 1278 | 1279 | /// Key is not found 1280 | if (buf[erased_key_position].isZero(*this)) 1281 | return false; 1282 | 1283 | /// We need to guarantee loop termination because there will be empty position 1284 | assert(m_size < grower.bufSize()); 1285 | 1286 | size_t next_position = erased_key_position; 1287 | 1288 | /** 1289 | * During element deletion there is a possibility that the search will be broken for one 1290 | * of the following elements, because this place erased_key_position is empty. We will check 1291 | * next_element. Consider a sequence from (erased_key_position, next_element], if the 1292 | * optimal_position of next_element falls into it, then removing erased_key_position 1293 | * will not break search for next_element. 1294 | * If optimal_position of the element does not fall into the sequence (erased_key_position, next_element] 1295 | * then deleting a erased_key_position will break search for it, so we need to move next_element 1296 | * to erased_key_position. Now we have empty place at next_element, so we apply the identical 1297 | * procedure for it. 1298 | * If an empty element is encountered then means that there is no more next elements for which we can 1299 | * break the search so we can exit. 1300 | */ 1301 | 1302 | /// Walk to the right through collision resolution chain and move elements to better positions 1303 | while (true) 1304 | { 1305 | next_position = grower.next(next_position); 1306 | 1307 | /// If there's no more elements in the chain 1308 | if (buf[next_position].isZero(*this)) 1309 | break; 1310 | 1311 | /// The optimal position of the element in the cell at next_position 1312 | size_t optimal_position = grower.place(buf[next_position].getHash(*this)); 1313 | 1314 | /// If position of this element is already optimal - proceed to the next element. 1315 | if (optimal_position == next_position) 1316 | continue; 1317 | 1318 | /// Cannot move this element because optimal position is after the freed place 1319 | /// The second condition is tricky - if the chain was overlapped before erased_key_position, 1320 | /// and the optimal position is actually before in collision resolution chain: 1321 | /// 1322 | /// [*xn***----------------***] 1323 | /// ^^-next elem ^ 1324 | /// | | 1325 | /// erased elem the optimal position of the next elem 1326 | /// 1327 | /// so, the next elem should be moved to position of erased elem 1328 | 1329 | /// The case of non overlapping part of chain 1330 | if (next_position > erased_key_position 1331 | && (optimal_position > erased_key_position) && (optimal_position < next_position)) 1332 | { 1333 | continue; 1334 | } 1335 | 1336 | /// The case of overlapping chain 1337 | if (next_position < erased_key_position 1338 | /// Cannot move this element because optimal position is after the freed place 1339 | && ((optimal_position > erased_key_position) || (optimal_position < next_position))) 1340 | { 1341 | continue; 1342 | } 1343 | 1344 | /// Move the element to the freed place 1345 | memcpy(static_cast(&buf[erased_key_position]), static_cast(&buf[next_position]), sizeof(Cell)); 1346 | 1347 | if constexpr (Cell::need_to_notify_cell_during_move) 1348 | Cell::move(&buf[next_position], &buf[erased_key_position]); 1349 | 1350 | /// Now we have another freed place 1351 | erased_key_position = next_position; 1352 | } 1353 | 1354 | buf[erased_key_position].setZero(); 1355 | --m_size; 1356 | 1357 | return true; 1358 | } 1359 | 1360 | bool ALWAYS_INLINE has(const Key & x) const 1361 | { 1362 | if (Cell::isZero(x, *this)) 1363 | return this->hasZero(); 1364 | 1365 | size_t hash_value = hash(x); 1366 | size_t place_value = findCell(x, hash_value, grower.place(hash_value)); 1367 | return !buf[place_value].isZero(*this); 1368 | } 1369 | 1370 | 1371 | bool ALWAYS_INLINE has(const Key & x, size_t hash_value) const 1372 | { 1373 | if (Cell::isZero(x, *this)) 1374 | return this->hasZero(); 1375 | 1376 | size_t place_value = findCell(x, hash_value, grower.place(hash_value)); 1377 | return !buf[place_value].isZero(*this); 1378 | } 1379 | 1380 | 1381 | // void write(DB::WriteBuffer & wb) const 1382 | // { 1383 | // Cell::State::write(wb); 1384 | // DB::writeVarUInt(m_size, wb); 1385 | 1386 | // if (this->hasZero()) 1387 | // this->zeroValue()->write(wb); 1388 | 1389 | // if (!buf) 1390 | // return; 1391 | 1392 | // for (auto ptr = buf, buf_end = buf + grower.bufSize(); ptr < buf_end; ++ptr) 1393 | // if (!ptr->isZero(*this)) 1394 | // ptr->write(wb); 1395 | // } 1396 | 1397 | // void writeText(DB::WriteBuffer & wb) const 1398 | // { 1399 | // Cell::State::writeText(wb); 1400 | // DB::writeText(m_size, wb); 1401 | 1402 | // if (this->hasZero()) 1403 | // { 1404 | // DB::writeChar(',', wb); 1405 | // this->zeroValue()->writeText(wb); 1406 | // } 1407 | 1408 | // if (!buf) 1409 | // return; 1410 | 1411 | // for (auto ptr = buf, buf_end = buf + grower.bufSize(); ptr < buf_end; ++ptr) 1412 | // { 1413 | // if (!ptr->isZero(*this)) 1414 | // { 1415 | // DB::writeChar(',', wb); 1416 | // ptr->writeText(wb); 1417 | // } 1418 | // } 1419 | // } 1420 | 1421 | // void read(DB::ReadBuffer & rb) 1422 | // { 1423 | // Cell::State::read(rb); 1424 | 1425 | // destroyElements(); 1426 | // m_size = 0; 1427 | 1428 | // size_t new_size = 0; 1429 | // DB::readVarUInt(new_size, rb); 1430 | // if (new_size > 100'000'000'000) 1431 | // throw DB::Exception(DB::ErrorCodes::TOO_LARGE_ARRAY_SIZE, "The size of serialized hash table is suspiciously large: {}", new_size); 1432 | 1433 | // free(); 1434 | // Grower new_grower = grower; 1435 | // new_grower.set(new_size); 1436 | // alloc(new_grower); 1437 | 1438 | // for (size_t i = 0; i < new_size; ++i) 1439 | // { 1440 | // Cell x; 1441 | // x.read(rb); 1442 | // insert(x.getValue()); 1443 | // } 1444 | // } 1445 | 1446 | // void readText(DB::ReadBuffer & rb) 1447 | // { 1448 | // Cell::State::readText(rb); 1449 | 1450 | // destroyElements(); 1451 | // m_size = 0; 1452 | 1453 | // size_t new_size = 0; 1454 | // DB::readText(new_size, rb); 1455 | 1456 | // free(); 1457 | // Grower new_grower = grower; 1458 | // new_grower.set(new_size); 1459 | // alloc(new_grower); 1460 | 1461 | // for (size_t i = 0; i < new_size; ++i) 1462 | // { 1463 | // Cell x; 1464 | // DB::assertChar(',', rb); 1465 | // x.readText(rb); 1466 | // insert(x.getValue()); 1467 | // } 1468 | // } 1469 | 1470 | 1471 | size_t size() const 1472 | { 1473 | return m_size; 1474 | } 1475 | 1476 | bool empty() const 1477 | { 1478 | return 0 == m_size; 1479 | } 1480 | 1481 | void clear() 1482 | { 1483 | destroyElements(); 1484 | m_size = 0; 1485 | 1486 | memset(static_cast(buf), 0, grower.bufSize() * sizeof(*buf)); 1487 | } 1488 | 1489 | /// After executing this function, the table can only be destroyed, 1490 | /// and also you can use the methods `size`, `empty`, `begin`, `end`. 1491 | void clearAndShrink() 1492 | { 1493 | destroyElements(); 1494 | m_size = 0; 1495 | free(); 1496 | } 1497 | 1498 | size_t getBufferSizeInBytes() const 1499 | { 1500 | return grower.bufSize() * sizeof(Cell); 1501 | } 1502 | 1503 | size_t getBufferSizeInCells() const 1504 | { 1505 | return grower.bufSize(); 1506 | } 1507 | 1508 | /// Return offset for result in internal buffer. 1509 | /// Result can have value up to `getBufferSizeInCells() + 1` 1510 | /// because offset for zero value considered to be 0 1511 | /// and for other values it will be `offset in buffer + 1` 1512 | size_t offsetInternal(ConstLookupResult ptr) const 1513 | { 1514 | if (ptr->isZero(*this)) 1515 | return 0; 1516 | return ptr - buf + 1; 1517 | } 1518 | 1519 | #ifdef DBMS_HASH_MAP_COUNT_COLLISIONS 1520 | size_t getCollisions() const 1521 | { 1522 | return collisions; 1523 | } 1524 | #endif 1525 | }; 1526 | --------------------------------------------------------------------------------