From ccc38a9a05cd4bf64592d8d9761b47508d64db12 Mon Sep 17 00:00:00 2001 From: JacksonAllan Date: Mon, 18 Mar 2024 03:07:18 +0300 Subject: [PATCH] Squashed commit of the following: commit 1e81fcbd5a1848bcbc8f9438ee6d1a0b7c5c32e5 Author: JacksonAllan Date: Mon Mar 18 01:37:14 2024 +0300 Update README.md commit 7fa5bc7c194b5a0ab570641afa5231355bbc2a91 Author: JacksonAllan Date: Mon Mar 18 01:35:48 2024 +0300 Update pre-release to full release commit cd6de7c2b5df195dfee0b70a4bb86a46d4c769b1 Author: JacksonAllan Date: Tue Feb 20 16:25:30 2024 +0300 Update cc.h commit 2ba9ad254aeda665511ba84985a2927b31664895 Author: JacksonAllan Date: Tue Feb 20 04:21:10 2024 +0300 v1.1.0 beta release commit 726da2fbe34f9d3b4b3cdc6ebf68257819619b58 Author: JacksonAllan Date: Sun Feb 18 16:17:41 2024 +0300 Added explicit BETA tag. commit 79b4e177757af6fe3e3b5f086055fe9187223320 Author: JacksonAllan Date: Sun Feb 18 04:34:35 2024 +0300 Revised code comments and documentation, add CC_LIKELY/UNLIKELY. commit b6df376cb0c94c5b6636f11c6ddb73ed8ba67427 Author: JacksonAllan Date: Mon Feb 12 03:06:53 2024 +0300 Updated readme, unit tests. commit 31d6ba448e55138d71c068f4e280423b055e53c8 Author: JacksonAllan Date: Fri Feb 9 20:57:35 2024 +0300 Update unit_tests.c commit 2d07ae66452f692e09ce67d6a3ecffb9725a8b58 Author: JacksonAllan Date: Fri Feb 9 20:39:56 2024 +0300 Update cc.h commit 5bfa266c3ba1873afba95be220a7d6f072467116 Author: JacksonAllan Date: Wed Feb 7 19:43:05 2024 +0300 Integrated Verstable Integrated Verstable (first fully working draft). --- README.md | 41 +- api_reference.md | 39 +- cc.h | 1419 ++++++++++++++++++++++------------- tests/tests_against_stl.cpp | 4 +- tests/unit_tests.c | 99 ++- 5 files changed, 1032 insertions(+), 570 deletions(-) diff --git a/README.md b/README.md index e9934f8..10d1d0d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ CC: Convenient Containers -*Note: [Version 1.1.0 Pre-Release](https://github.com/JacksonAllan/CC/releases/tag/v1.1.0_Pre-Release) is now available via a [dedicated branch](https://github.com/JacksonAllan/CC/tree/v1.1.0-Pre-Release).* - -**Convenient Containers** (**CC**) is a small, usability-oriented generic container library for C that provides **vectors**, **doubly linked lists**, **unordered maps**, and **unordered sets**. +Convenient Containers (**CC**) is a small, usability-oriented generic container library for C that provides **vectors**, **doubly linked lists**, **unordered maps**, and **unordered sets**. Its features include: -- Fully generic API. -- Type safety. -- User-defined destructor, comparison, and hash functions associated with element and key types. -- No assumption of successful memory allocation. -- Single header. -- Compiles in C and C++. +* Fully generic API. +* Type safety. +* User-defined destructor, comparison, and hash functions associated with element and key types. +* No assumption of successful memory allocation. +* Single header. +* Compiles in C and C++. It requires C23, or C11 and compiler support for `typeof`, or C++11. @@ -29,7 +27,7 @@ In contrast, **CC** requires no type definitions and provides an API agnostic to - + ```c // CC @@ -50,14 +48,10 @@ int main( void ) printf( "%f\n", *get( &our_map, 5 ) ); cleanup( &our_map ); } - - - - ``` - + ```c // Template-instantiation paradigm @@ -87,7 +81,7 @@ int main( void ) - + ```c // Typed-pointer/hidden-metadata paradigm @@ -115,7 +109,7 @@ int main( void ) ``` - + ```c // void-pointers paradigm @@ -136,11 +130,6 @@ int main( void ) printf( "%f\n", *(float *)map_get( &our_map, &(int){ 5 } ) ); map_cleanup( &our_map ); } - - - - - ``` @@ -249,7 +238,7 @@ int main( void ) ### Map -A `map` is an unordered container associating elements with keys, implemented as a [Robin Hood](https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation) hash table. +A `map` is an unordered associative container mapping elements to keys, implemented as a hybird open-addressing, chained hash table that is also available as a [standalone library](https://github.com/JacksonAllan/Verstable). ```c #include @@ -301,7 +290,7 @@ int main( void ) ### Set -A `set` is a [Robin Hood](https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation) hash table for elements without a separate key. +A `set` is an unordered associative container for elements without a separate key, implemented as a hybird open-addressing, chained hash table also available as a [standalone library](https://github.com/JacksonAllan/Verstable). ```c #include @@ -503,4 +492,6 @@ Destructor, comparison, and hash functions are also deduced via a novel techniqu Future versions should include `NULL`-terminated dynamic strings, ordered maps, and ordered sets, as well as performance benchmarks. -## [API Reference](api_reference.md) +## API Reference + +Full API documentation is available [here](api_reference.md). \ No newline at end of file diff --git a/api_reference.md b/api_reference.md index b800ea4..aa3e2bd 100644 --- a/api_reference.md +++ b/api_reference.md @@ -96,7 +96,7 @@ Returns the current capacity. bool reserve( vec( el_ty ) *cntr, size_t n ) ``` -Ensures that the the capacity is large enough to support `n` elements. +Ensures that the the capacity is large enough to accommodate `n` elements. Returns `true`, or `false` if unsuccessful due to memory allocation failure. ```c @@ -287,9 +287,9 @@ It is equivalent to `for( el_ty *i_name = last( cntr ); i_name != r_end( cntr ); > **Note** > List pointer-iterators (including `r_end` and `end`) are not invalidated by any API calls besides `init` and `cleanup`, unless they point to erased elements. -### Map +## Map -A `map` is an unordered container associating elements with keys, implemented as a Robin Hood hash table. +A `map` is an unordered associative container mapping elements to keys, implemented as a hybird open-addressing, chained hash table. ```c map( key_ty, el_ty ) cntr @@ -305,13 +305,13 @@ size_t cap( map( key_ty, el_ty ) *cntr ) ``` Returns the current capacity, i.e. bucket count. -Note that the number of elements a map can support without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. +Note that the number of elements a map can accommodate without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. ```c bool reserve( map( key_ty, el_ty ) *cntr, size_t n ) ``` -Ensures that the capacity is large enough to support `n` elements without rehashing. +Ensures that the capacity is large enough to accommodate `n` elements without rehashing. Returns `true`, or `false` if unsuccessful due to memory allocation failure. ```c @@ -327,8 +327,7 @@ el_ty *insert( map( key_ty, el_ty ) *cntr, key_ty key, el_ty el ) Inserts element `el` with the specified key. If an element with the same key already exists, the existing element is replaced. -Returns a pointer-iterator to the new element, or `NULL` in the case of memory allocation failure. -If adding one element would violate the map's max load factor, failure can occur even if it already contains the key. +Returns a pointer-iterator to the new element, or `NULL` in the case of memory allocation failure. ```c el_ty *get( map( key_ty, el_ty ) *cntr, key_ty key ) @@ -341,9 +340,7 @@ el_ty *get_or_insert( map( key_ty, el_ty ) *cntr, key_ty key, el_ty el ) ``` Inserts element `el` if no element with the specified key already exist. -Returns a pointer-iterator to the new element if it was inserted, or a pointer-iterator to the existing element with the same key, or `NULL` in the case of memory allocation failure. -If adding one element would violate the map's max load factor, failure can occur even if it already contains the key. -Determine whether an element was inserted by comparing the map's size before and after the call. +Returns a pointer-iterator to the new element if it was inserted, or a pointer-iterator to the existing element with the same key, or `NULL` in the case of memory allocation failure. ```c const key_ty *key_for( map( key_ty, el_ty ) *cntr, el_ty *i ) @@ -359,10 +356,11 @@ Erases the element with the specified key, if it exists. Returns `true` if an element was erased, or `false` if no such element exists. ```c -void erase_itr( map( key_ty, el_ty ) *cntr, el_ty *i ) +el_ty * erase_itr( map( key_ty, el_ty ) *cntr, el_ty *i ) ``` -Erases the element pointed to by pointer-iterator `i`. +Erases the element pointed to by pointer-iterator `i`. +Returns an iterator to the next element in the map, or an end pointer-iterator if the erased element was the last one. ```c el_ty *first( map( key_ty, el_ty ) *cntr ) @@ -433,7 +431,7 @@ It should be followed by the body of the loop. ## Set -A `set` is a Robin Hood hash table for elements without a separate key. +A `set` is an unordered associative container for elements without a separate key, implemented as a hybird open-addressing, chained hash table ```c set( el_ty ) cntr @@ -449,13 +447,13 @@ size_t cap( set( el_ty ) *cntr ) ``` Returns the current capacity, i.e. bucket count. -Note that the number of elements a set can support without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. +Note that the number of elements a set can accommodate without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. ```c bool reserve( set( el_ty ) *cntr, size_t n ) ``` -Ensures that the capacity is large enough to support `n` elements without rehashing. +Ensures that the capacity is large enough to accommodate `n` elements without rehashing. Returns `true`, or `false` if unsuccessful due to memory allocation failure. ```c @@ -471,8 +469,7 @@ el_ty *insert( set( el_ty ) *cntr, el_ty el ) Inserts element `el`. If the element already exists, the existing element is replaced. -Returns a pointer-iterator to the new element, or `NULL` in the case of memory allocation failure. -Note that if adding one element would violate the set's max load factor, failure can occur even if it already contains `el`. +Returns a pointer-iterator to the new element, or `NULL` in the case of memory allocation failure. ```c el_ty *get( set( el_ty ) *cntr, el_ty el ) @@ -486,7 +483,6 @@ el_ty *get_or_insert( set( el_ty ) *cntr, el_ty el ) Inserts element `el` if it does not already exist. Returns a pointer-iterator to the new element if it was inserted, or a pointer-iterator to the existing element, or `NULL` in the case of memory allocation failure. -If adding one element would violate the set's max load factor, failure can occur even if it already contains the element. Determine whether an element was inserted by comparing the set's size before and after the call. ```c @@ -496,6 +492,13 @@ bool erase( set( el_ty ) *cntr, el_ty el ) Erases the element `el`, if it exists. Returns `true` if an element was erased, or `false` if no such element exists. +```c +el_ty * erase_itr( set( el_ty ) *cntr, el_ty *i ) +``` + +Erases the element pointed to by pointer-iterator `i`. +Returns an iterator to the next element in the set, or an end pointer-iterator if the erased element was the last one. + ```c el_ty *first( set( el_ty ) *cntr ) ``` diff --git a/cc.h b/cc.h index 30844b3..8412712 100644 --- a/cc.h +++ b/cc.h @@ -1,4 +1,4 @@ -/*----------------------------------------- CC: CONVENIENT CONTAINERS v1.0.4 ------------------------------------------- +/*----------------------------------------- CC: CONVENIENT CONTAINERS v1.1.0 ------------------------------------------- This library provides usability-oriented generic containers (vectors, linked lists, unordered maps, and unordered sets). @@ -48,7 +48,7 @@ Tested with GCC, MinGW, and Clang. Hence, only create a duplicate via assignment (including through function parameters and return values) if you have finished with the original. - An iterator is a pointer to an element in the container or to the associated end (or r_end, if the container - supports it). In the documentation below, these pointers are called "pointer-iterators". + supports it). In the documentation below, these pointers are referred to as "pointer-iterators". - In the documentation below, el_ty is the container's element type and key_ty is the container's key type (where applicable). @@ -99,7 +99,7 @@ Tested with GCC, MinGW, and Clang. bool reserve( vec( el_ty ) *cntr, size_t n ) - Ensures that the the capacity is large enough to support n elements. + Ensures that the the capacity is large enough to accommodate n elements. Returns true, or false if unsuccessful due to memory allocation failure. bool resize( vec( el_ty ) *cntr, size_t n ) @@ -247,26 +247,24 @@ Tested with GCC, MinGW, and Clang. - List pointer-iterators (including r_end and end) are not invalidated by any API calls besides init and cleanup, unless they point to erased elements. - Map (an unordered container associating elements with keys, implemented as a Robin Hood hash table): + Map (an unordered associative container mapping elements to keys, implemented as a hybird open-addressing, chained + hash table): map( key_ty, el_ty ) cntr Declares an uninitialized map named cntr. - key_ty must be a type - or alias for a type - for which comparison and hash functions have been defined and whose - size is < 4,294,967,295 bytes (~4.3GB) and alignment is <= 256. - These requirements are enforced internally such that neglecting them causes a compiler error. For types with in-built comparison and hash functions, and for details on how to declare new comparison and hash functions, see "Destructor, comparison, and hash functions and custom max load factors" below. size_t cap( map( key_ty, el_ty ) *cntr ) Returns the current capacity, i.e. bucket count. - Note that the number of elements a map can support without rehashing is not its capacity but its capacity + Note that the number of elements a map can accommodate without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. bool reserve( map( key_ty, el_ty ) *cntr, size_t n ) - Ensures that the capacity is large enough to support n elements without rehashing. + Ensures that the capacity is large enough to accommodate n elements without rehashing. Returns true, or false if unsuccessful due to memory allocation failure. bool shrink( map( key_ty, el_ty ) *cntr ) @@ -279,8 +277,6 @@ Tested with GCC, MinGW, and Clang. Inserts element el with the specified key. If an element with the same key already exists, the existing element is replaced. Returns a pointer-iterator to the new element, or NULL in the case of memory allocation failure. - If adding one element would violate the map's max load factor, failure can occur even if it already contains the - key. el_ty *get( map( key_ty, el_ty ) *cntr, key_ty key ) @@ -291,8 +287,6 @@ Tested with GCC, MinGW, and Clang. Inserts element el if no element with the specified key already exist. Returns a pointer-iterator to the new element if it was inserted, or a pointer-iterator to the existing element with the same key, or NULL in the case of memory allocation failure. - If adding one element would violate the map's max load factor, failure can occur even if it already contains the - key. Determine whether an element was inserted by comparing the map's size before and after the call. const key_ty *key_for( map( key_ty, el_ty ) *cntr, el_ty *i ) @@ -304,9 +298,11 @@ Tested with GCC, MinGW, and Clang. Erases the element with the specified key, if it exists. Returns true if an element was erased, or false if no such element exists. - void erase_itr( map( key_ty, el_ty ) *cntr, el_ty *i ) + el_ty *erase_itr( map( key_ty, el_ty ) *cntr, el_ty *i ) Erases the element pointed to by pointer-iterator i. + Returns a pointer-iterator to the next element in the map, or an end pointer-iterator if the erased element was + the last one. el_ty *first( map( key_ty, el_ty ) *cntr ) @@ -364,26 +360,24 @@ Tested with GCC, MinGW, and Clang. - Map pointer-iterators (including r_end and end) may be invalidated by any API calls that cause memory reallocation. - Set (Robin Hood hash table for elements without a separate key): + Set (an unordered associative container for elements without a separate key, implemented as a hybird open-addressing, + chained hash table): set( el_ty ) cntr Declares an uninitialized set named cntr. - el_ty must be a type - or alias for a type - for which comparison and hash functions have been defined and whose - size is < 4,294,967,295 bytes (~4.3GB) and alignment is <= 256. - These requirements are enforced internally such that neglecting them causes a compiler error. For types with in-built comparison and hash functions, and for details on how to declare new comparison and hash functions, see "Destructor, comparison, and hash functions and custom max load factors" below. size_t cap( set( el_ty ) *cntr ) Returns the current capacity, i.e. bucket count. - Note that the number of elements a set can support without rehashing is not its capacity but its capacity + Note that the number of elements a set can accommodate without rehashing is not its capacity but its capacity multiplied by the max load factor associated with its key type. bool reserve( set( el_ty ) *cntr, size_t n ) - Ensures that the capacity is large enough to support n elements without rehashing. + Ensures that the capacity is large enough to accommodate n elements without rehashing. Returns true, or false if unsuccessful due to memory allocation failure. bool shrink( set( el_ty ) *cntr ) @@ -396,8 +390,6 @@ Tested with GCC, MinGW, and Clang. Inserts element el. If the element already exists, the existing element is replaced. Returns a pointer-iterator to the new element, or NULL in the case of memory allocation failure. - Note that if adding one element would violate the set's max load factor, failure can occur even if it already - contains el. el_ty *get( set( el_ty ) *cntr, el_ty el ) @@ -408,8 +400,6 @@ Tested with GCC, MinGW, and Clang. Inserts element el if it does not already exist. Returns a pointer-iterator to the new element if it was inserted, or a pointer-iterator to the existing element, or NULL in the case of memory allocation failure. - If adding one element would violate the set's max load factor, failure can occur even if it already contains the - element. Determine whether an element was inserted by comparing the set's size before and after the call. bool erase( set( el_ty ) *cntr, el_ty el ) @@ -417,6 +407,12 @@ Tested with GCC, MinGW, and Clang. Erases the element el, if it exists. Returns true if an element was erased, or false if no such element exists. + el_ty *erase_itr( set( el_ty ) *cntr, el_ty *i ) + + Erases the element pointed to by pointer-iterator i. + Returns an pointer-iterator to the next element in the set, or an end pointer-iterator if the erased element was + the last one. + el_ty *first( set( el_ty ) *cntr ) Returns a pointer-iterator to the first element, or an end pointer-iterator if the set is empty. @@ -518,6 +514,9 @@ Tested with GCC, MinGW, and Clang. Version history: + 18/03/2024 1.1.0: Replaced the Robin Hood implementations of map and set with Verstable implementations. + Added branch-prediction optimizations. + Improved documentation. 23/01/2024 1.0.4: Fixed critical bug causing undefined behavior upon iteration of empty maps with nonzero capacities. Fixed formatting inconsistencies and improved code comments. 04/05/2023 1.0.3: Completed refractor that reduces compile speed by approximately 53% in C with GCC. @@ -553,6 +552,7 @@ License (MIT): WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #if !defined( CC_DTOR ) && !defined( CC_CMPR ) && !defined( CC_HASH ) && !defined( CC_LOAD )/*------------------------*/ @@ -599,6 +599,7 @@ License (MIT): #ifndef CC_H #define CC_H +#include #include #include #include @@ -648,6 +649,15 @@ template ty_1 cc_maybe_unused( ty_2 xp ){ return ( #define CC_ALWAYS_INLINE #endif +// Branch optimization macros. +#ifdef __GNUC__ +#define CC_LIKELY( xp ) __builtin_expect( (bool)( xp ), true ) +#define CC_UNLIKELY( xp ) __builtin_expect( (bool)( xp ), false ) +#else +#define CC_LIKELY( xp ) ( xp ) +#define CC_UNLIKELY( xp ) ( xp ) +#endif + // CC_IF_THEN_CAST_TY_1_ELSE_CAST_TY_2 is the same as above, except that it selects the type to which to cast based on // a condition. // This is necessary because some API macros (e.g. cc_erase) return either a pointer-iterator or a bool depending on the @@ -744,7 +754,7 @@ CC_CAST_MAYBE_UNUSED( \ #endif #endif -// Some functions that must return true/false must return the value in the form of a pointer. +// Some functions that must return true or false must return the value in the form of a pointer. // This is because they are paired in ternary expressions inside API macros with other functions for other containers // that return a pointer (primarily cc_erase). // While any suitably aligned pointer - e.g. the container handle - would do, we declare a global cc_dummy_true_ptr for @@ -753,7 +763,7 @@ static max_align_t cc_dummy_true; static void *cc_dummy_true_ptr = &cc_dummy_true; // Default max load factor for maps and sets. -#define CC_DEFAULT_LOAD 0.75 +#define CC_DEFAULT_LOAD 0.9 // Types for comparison, hash, destructor, realloc, and free functions. // These are only for internal use as user-provided comparison, hash, and destructor functions have different signatures @@ -811,7 +821,7 @@ typedef void ( *cc_free_fnptr_ty )( void * ); ) ? 1 : -1 ) \ ) \ -// Retrieves a container's id (CC_VEC, CC_LIST, etc.) from its handle. +// Retrieves a container's id (e.g. CC_VEC) from its handle. #define CC_CNTR_ID( cntr ) ( sizeof( *cntr ) / sizeof( **cntr ) ) // Retrieves a container's element type from its handle. @@ -863,16 +873,10 @@ CC_TYPEOF_XP( #endif -// Probe length type for maps and sets (Robin Hood hash tables). -// An unsigned char would probably be fine, but we use unsigned int just in case. -// A probe length of 0 denotes an empty bucket, whereas a probe length of 1 denotes an element in its home bucket. -// This optimization allows us to eliminate separate checks for empty buckets. -typedef unsigned int cc_probelen_ty; - // The functions associated with some containers require extra information about how elements and/or keys and other data // are laid out in memory. -// Specifically, maps and sets need information about their bucket layouts, which depend on their element and/or key -// types (a future implementation of ordered maps and sets will require similar data). +// Specifically, maps - and by extension sets - need information about their bucket layouts, which depend on their +// element and/or key types (a future implementation of ordered maps and sets will require similar information). // This data is formed by extracting the key size and alignment and passing it, along with the element size and // alignment and the container type id, into the cc_layout function, which returns a uint64_t describing the layout. // The key size and alignment are inferred via a _Generic macro that looks up the key type based on the default @@ -883,40 +887,34 @@ typedef unsigned int cc_probelen_ty; // The compiler should optimize the entire mechanism into compile-time constants. // The layout for a map bucket is: -// +------------+----+------------+----+------------+----+ -// | #1 | #2 | #3 | #4 | #5 | #6 | -// +------------+----+------------+----+------------+----+ +// +------------+----+------------+----+ +// | #1 | #2 | #3 | #4 | +// +------------+----+------------+----+ // #1 Element. // #2 Element padding to key_ty alignment. // #3 Key. -// #4 Key padding to cc_probelen_ty alignment. -// #5 Probe length. -// #6 Padding to largest of el_ty, key_ty, and cc_probelen_ty alignments. +// #4 Key padding to the larger of el_ty and key_ty alignments. -// The layout for a set bucket is: -// +------------+----+------------+----+ -// | #1 | #2 | #3 | #4 | -// +------------+----+------------+----+ -// #1 Element (el_ty and key_ty are the same). -// #2 Padding to cc_probelen_ty alignment. -// #3 Probe length. -// #4 Padding to the larger of el_ty and cc_probelen_ty alignments. +// For sets, the layout collapses to just: +// +------------+ +// | #1 | +// +------------+ +// #1 Element. // The layout data passed into a container function is a uint64_t composed of a uint32_t denoting the key size, a -// uint8_t denoting the padding after the element, a uint8_t denoting the padding after the key, and a uint8_t -// denoting the padding after the probe length. +// uint16_t denoting the padding after the element, and a uint16_t denoting the padding after the key. // The reason that a uint64_t, rather than a struct, is used is that GCC seems to have trouble properly optimizing the // passing of the struct - even if only 8 bytes - into some container functions (specifically cc_map_insert), apparently // because it declines to pass by register. // Macro for ensuring valid layout on container declaration. -// Since the key size occupies four bytes and the padding values each occupy one byte, the key size must be <= -// UINT32_MAX (about 4.3GB) and the alignment of the element and key must be <= UINT8_MAX + 1 (i.e. 256). +// Since the key size occupies four bytes and the padding values each occupy two bytes, the key size must be <= +// UINT32_MAX (about 4.3GB) and the alignment of the element and key must be <= UINT16_MAX + 1 (i.e. 65536). // It unlikely that these constraints would be violated in practice, but we can check anyway. -#define CC_SATISFIES_LAYOUT_CONSTRAINTS( key_ty, el_ty ) \ -( sizeof( key_ty ) <= UINT32_MAX && alignof( el_ty ) <= UINT8_MAX + 1 && alignof( key_ty ) <= UINT8_MAX + 1 ) \ +#define CC_SATISFIES_LAYOUT_CONSTRAINTS( key_ty, el_ty ) \ +( sizeof( key_ty ) <= UINT32_MAX && alignof( el_ty ) <= UINT16_MAX + 1 && alignof( key_ty ) <= UINT16_MAX + 1 ) \ -// Macros and functions for constructing a layout. +// Macros and functions for constructing a map bucket layout. #define CC_PADDING( size, align ) ( ( ~(size) + 1 ) & ( (align) - 1 ) ) #define CC_MAX( a, b ) ( (a) > (b) ? (a) : (b) ) @@ -924,24 +922,8 @@ typedef unsigned int cc_probelen_ty; #define CC_MAP_EL_PADDING( el_size, key_align ) \ CC_PADDING( el_size, key_align ) \ -#define CC_MAP_KEY_PADDING( el_size, key_size, key_align ) \ -CC_PADDING( el_size + CC_MAP_EL_PADDING( el_size, key_align ) + key_size, alignof( cc_probelen_ty ) ) \ - -#define CC_MAP_PROBELEN_PADDING( el_size, el_align, key_size, key_align ) \ -CC_PADDING( \ - el_size + CC_MAP_EL_PADDING( el_size, key_align ) + key_size + \ - CC_MAP_KEY_PADDING( el_size, key_size, key_align ) + sizeof( cc_probelen_ty ), \ - CC_MAX( key_align, CC_MAX( el_align, alignof( cc_probelen_ty ) ) ) \ -) \ - -#define CC_SET_EL_PADDING( el_size ) \ -CC_PADDING( el_size, alignof( cc_probelen_ty ) ) \ - -#define CC_SET_PROBELEN_PADDING( el_size, el_align ) \ -CC_PADDING( \ - el_size + CC_SET_EL_PADDING( el_size ) + sizeof( cc_probelen_ty ), \ - CC_MAX( el_align, alignof( cc_probelen_ty ) ) \ -) \ +#define CC_MAP_KEY_PADDING( el_size, el_align, key_size, key_align ) \ +CC_PADDING( el_size + CC_MAP_EL_PADDING( el_size, key_align ) + key_size, CC_MAX( el_align, key_align ) ) \ // Struct for conveying key-related information from the _Generic macro into the function below. typedef struct @@ -961,17 +943,12 @@ static inline CC_ALWAYS_INLINE uint64_t cc_layout( { if( cntr_id == CC_MAP ) return - key_details.size | - CC_MAP_EL_PADDING( el_size, key_details.align ) << 32 | - CC_MAP_KEY_PADDING( el_size, key_details.size, key_details.align ) << 40 | - CC_MAP_PROBELEN_PADDING( el_size, el_align, key_details.size, key_details.align ) << 48; + key_details.size | + CC_MAP_EL_PADDING( el_size, key_details.align ) << 32 | + CC_MAP_KEY_PADDING( el_size, el_align, key_details.size, key_details.align ) << 48; if( cntr_id == CC_SET ) - return - el_size | - (uint64_t)0 << 32 | - CC_SET_EL_PADDING( el_size ) << 40 | - CC_SET_PROBELEN_PADDING( el_size, el_align ) << 48; + return el_size; return 0; // Other container types don't require layout data. } @@ -980,13 +957,10 @@ static inline CC_ALWAYS_INLINE uint64_t cc_layout( #define CC_KEY_SIZE( layout ) (uint32_t)( layout ) -#define CC_KEY_OFFSET( el_size, layout ) ( (el_size) + (uint8_t)( layout >> 32 ) ) +#define CC_KEY_OFFSET( el_size, layout ) ( (el_size) + (uint16_t)( layout >> 32 ) ) -#define CC_PROBELEN_OFFSET( el_size, layout ) \ -( CC_KEY_OFFSET( el_size, layout ) + (uint32_t)( layout ) + (uint8_t)( layout >> 40 ) ) \ - -#define CC_BUCKET_SIZE( el_size, layout ) \ -( CC_PROBELEN_OFFSET( el_size, layout ) + sizeof( cc_probelen_ty ) + (uint8_t)( layout >> 48 ) ) \ +#define CC_BUCKET_SIZE( el_size, layout ) \ +( CC_KEY_OFFSET( el_size, layout ) + (uint32_t)( layout ) + (uint16_t)( layout >> 48 ) ) \ // Return type for all functions that could reallocate a container's memory. // It contains a new container handle (the pointer may have changed to due reallocation) and an additional pointer whose @@ -1044,6 +1018,73 @@ cc_memcpy_and_return_ptr( ( (cc_allocing_fn_result_ty *)cntr )->other_ptr \ ) \ +// Functions to find the first and last non-zero uint16_t in a uint64_t. +// These functions are used when we scan four buckets at a time while iterating over maps and sets. +// They rely on compiler intrinsics if possible. + +#if defined( __GNUC__ ) && ULLONG_MAX == 0xFFFFFFFFFFFFFFFF + +// The compiler will optimize away the endian check at -O1 and above. +static inline bool cc_is_little_endian( void ) +{ + const uint16_t endian_checker = 0x0001; + return *(char *)&endian_checker; +} + +static inline int cc_first_nonzero_uint16( uint64_t a ) +{ + if( cc_is_little_endian() ) + return __builtin_ctzll( a ) / 16; + + return __builtin_clzll( a ) / 16; +} + +static inline int cc_last_nonzero_uint16( uint64_t a ) +{ + if( cc_is_little_endian() ) + return __builtin_clzll( a ) / 16; + + return __builtin_ctzll( a ) / 16; +} + +#else + +static inline int cc_first_nonzero_uint16( uint64_t a ) +{ + int result = 0; + + uint32_t half; + memcpy( &half, &a, sizeof( uint32_t ) ); + if( !half ) + result += 2; + + uint16_t quarter; + memcpy( &quarter, (char *)&a + result * sizeof( uint16_t ), sizeof( uint16_t ) ); + if( !quarter ) + result += 1; + + return result; +} + +static inline int cc_last_nonzero_uint16( uint64_t a ) +{ + int result = 3; + + uint32_t half; + memcpy( &half, (char *)&a + sizeof( uint32_t ), sizeof( uint32_t ) ); + if( !half ) + result -= 2; + + uint16_t quarter; + memcpy( &quarter, (char *)&a + result * sizeof( uint16_t ), sizeof( uint16_t ) ); + if( !quarter ) + result -= 1; + + return 3 - result; +} + +#endif + /*--------------------------------------------------------------------------------------------------------------------*/ /* Vector */ /*--------------------------------------------------------------------------------------------------------------------*/ @@ -1094,7 +1135,7 @@ static inline void *cc_vec_get( return (char *)cntr + sizeof( cc_vec_hdr_ty ) + el_size * *(size_t *)key; } -// Ensures that the capacity is large enough to support n elements without reallocation. +// Ensures that the capacity is large enough to accommodate n elements without reallocation. // Returns a cc_allocing_fn_result_ty containing the new handle and a pointer that evaluates to true if the operation // was successful. static inline cc_allocing_fn_result_ty cc_vec_reserve( @@ -1118,7 +1159,7 @@ static inline cc_allocing_fn_result_ty cc_vec_reserve( sizeof( cc_vec_hdr_ty ) + el_size * n ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); if( is_placeholder ) @@ -1163,7 +1204,7 @@ static inline cc_allocing_fn_result_ty cc_vec_insert_n( realloc_, NULL // Dummy. ); - if( !result.other_ptr ) + if( CC_UNLIKELY( !result.other_ptr ) ) return result; cntr = result.new_cntr; @@ -1299,7 +1340,7 @@ static inline cc_allocing_fn_result_ty cc_vec_resize( realloc_, NULL // Dummy. ); - if( !result.other_ptr ) + if( CC_UNLIKELY( !result.other_ptr ) ) return result; cc_vec_hdr( result.new_cntr )->size = n; @@ -1328,7 +1369,7 @@ static inline cc_allocing_fn_result_ty cc_vec_shrink( } cc_vec_hdr_ty *new_cntr = (cc_vec_hdr_ty *)realloc_( cntr, sizeof( cc_vec_hdr_ty ) + el_size * cc_vec_size( cntr ) ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); cc_vec_hdr( new_cntr )->cap = cc_vec_size( new_cntr ); @@ -1358,7 +1399,7 @@ static inline void *cc_vec_init_clone( realloc_ ); - if( !result.other_ptr ) + if( CC_UNLIKELY( !result.other_ptr ) ) return NULL; memcpy( @@ -1467,8 +1508,8 @@ typedef struct } cc_list_hdr_ty; // Placeholder for a list with no allocated header. -// The main purpose this serves is to provide every list with stable r_end and end iterators across translation units -// and irrespective of whether any memory has been allocated for its header. +// The main purpose this serves is to provide every list with stable r_end and end pointer-iterators across translation +// units and irrespective of whether any memory has been allocated for its header. // Every list initially (after cc_init) points to this placeholder, which differs across translation units, and is then // associated with that placeholder until cc_cleanup is called. // Calls to cc_r_end and cc_end on a list return pointers to the associated placeholder's r_end and end nodes even after @@ -1543,8 +1584,8 @@ static inline void *cc_list_prev( { cc_listnode_hdr_ty *prev = cc_listnode_hdr( itr )->prev; - // If itr is r_end, then we need to decrement the iterator once more to ensure that the returned iterator is the r_end - // of the placeholder originally associated with the list. + // If itr is r_end, then we need to decrement the pointer-iterator once more to ensure that the returned + // pointer-iterator is the r_end of the placeholder originally associated with the list. if( prev == &cc_list_hdr( cntr )->r_end ) prev = prev->prev; @@ -1603,7 +1644,7 @@ static inline void *cc_list_alloc_hdr( ) { cc_list_hdr_ty *new_cntr = (cc_list_hdr_ty *)realloc_( NULL, sizeof( cc_list_hdr_ty ) ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return NULL; new_cntr->r_end.next = &new_cntr->end; @@ -1624,8 +1665,8 @@ static inline void cc_list_attach( cc_listnode_hdr_ty *node ) { - // Handle r_end and end iterators as a special case. - // We need to convert the iterator from the global placeholder's r_end or end to the local r_end or end. + // Handle r_end and end pointer-iterators as a special case. + // We need to convert the pointer-iterator from the global placeholder's r_end or end to the local r_end or end. if( itr == cc_list_r_end( cntr ) ) itr = cc_list_el( &cc_list_hdr( cntr )->r_end ); else if( itr == cc_list_end( cntr, 0 /* Dummy */, 0 /* Dummy */ ) ) @@ -1660,20 +1701,20 @@ static inline cc_allocing_fn_result_ty cc_list_insert( if( cc_list_is_placeholder( cntr ) ) { void *new_cntr = cc_list_alloc_hdr( cntr, realloc_ ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); cntr = new_cntr; } cc_listnode_hdr_ty *new_node = (cc_listnode_hdr_ty *)realloc_( NULL, sizeof( cc_listnode_hdr_ty ) + el_size ); - if( !new_node ) + if( CC_UNLIKELY( !new_node ) ) return cc_make_allocing_fn_result( cntr, NULL ); memcpy( cc_list_el( new_node ), el, el_size ); - // Handle r_end and end iterators as a special case. - // We need to convert the iterator from the associated placeholder's r_end or end to the local r_end or end. + // Handle r_end and end pointer-iterators as a special case. + // We need to convert the pointer-iterator from the associated placeholder's r_end or end to the local r_end or end. if( *(void **)key == cc_list_r_end( cntr ) ) *(void **)key = cc_list_el( &cc_list_hdr( cntr )->r_end ); else if( *(void **)key == cc_list_end( cntr, 0 /* Dummy */, 0 /* Dummy */ ) ) @@ -1759,7 +1800,7 @@ static inline cc_allocing_fn_result_ty cc_list_splice( if( cc_list_is_placeholder( cntr ) ) { void *new_cntr = cc_list_alloc_hdr( cntr, realloc_ ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); cntr = new_cntr; @@ -1881,19 +1922,102 @@ static inline void cc_list_cleanup( /* Map */ /*--------------------------------------------------------------------------------------------------------------------*/ +// cc_map implements Verstable (https://github.com/JacksonAllan/Verstable) within the constraints of the CC API. +// As explained in its documentation, Verstable is an open-addressing hash table using quadratic probing and the +// following additions: +// +// - All keys that hash (i.e. "belong") to the same bucket (their "home bucket") are linked together by an 11-bit +// integer specifying the quadratic displacement, relative to that bucket, of the next key in the chain. +// +// - If a chain of keys exists for a given bucket, then it always begins at that bucket. To maintain this policy, a +// 1-bit flag is used to mark whether the key occupying a bucket belongs there. When inserting a new key, if the +// bucket it belongs to is occupied by a key that does not belong there, then the occupying key is evicted and the new +// key takes the bucket. +// +// - A 4-bit fragment of each key's hash code is also stored. +// +// - The aforementioned metadata associated with each bucket (the 4-bit hash fragment, the 1-bit flag, and the 11-bit +// link to the next key in the chain) are stored together in a uint16_t array rather than in the bucket alongside the +// key and (optionally) the value. +// +// One way to conceptualize this scheme is as a chained hash table in which overflowing keys are stored not in separate +// memory allocations but in otherwise unused buckets. In this regard, it shares similarities with Malte Skarupke's +// Bytell hash table (https://www.youtube.com/watch?v=M2fKMP47slQ) and traditional "coalesced hashing". +// +// Advantages of this scheme include: +// +// - Fast lookups impervious to load factor: If the table contains any key belonging to the lookup key's home bucket, +// then that bucket contains the first in a traversable chain of all keys belonging to it. Hence, only the home bucket +// and other buckets containing keys belonging to it are ever probed. Moreover, the stored hash fragments allow +// skipping most non-matching keys in the chain without accessing the actual buckets array or calling the (potentially +// expensive) key comparison function. +// +// - Fast insertions: Insertions are faster than they are in other schemes that move keys around (e.g. Robin Hood) +// because they only move, at most, one existing key. +// +// - Fast, tombstone-free deletions: Deletions, which usually require tombstones in quadratic-probing hash tables, are +// tombstone-free and only move, at most, one existing key. +// +// - Fast iteration: The separate metadata array allows keys in sparsely populated tables to be found without incurring +// the frequent cache misses that would result from traversing the buckets array. +// +// Adapting Verstable to the CC API necessitates some compromises, including: +// +// - Slower iteration: As a pointer-iterator in CC cannot store a pointer to the metadatum associated with the bucket to +// which it points, the metadatum's address must be calculated every iteration. +// +// - Slower iterator-based erasure: As CC pointer-iterators cannot store a key's home bucket, CC must rehash the key +// being deleted in cases wherein Verstable does not need to. + +// Masks for manipulating and extracting data from a bucket's uint16_t metadatum. +#define CC_MAP_EMPTY 0x0000 +#define CC_MAP_HASH_FRAG_MASK 0xF000 // 0b1111000000000000. +#define CC_MAP_IN_HOME_BUCKET_MASK 0x0800 // 0b0000100000000000. +#define CC_MAP_DISPLACEMENT_MASK 0x07FF // 0b0000011111111111, also denotes the displacement limit. Set to CC_LOAD to + // 1.0 to test proper handling of encroachment on the displacement limit + // during inserts. + +// Extracts a hash fragment from a uint64_t hash code. +// We take the highest four bits so that keys that map (via modulo) to the same bucket have distinct hash fragments. +static inline uint16_t cc_hash_frag( size_t hash ) +{ + return ( hash >> ( sizeof( size_t ) - sizeof( uint16_t ) ) * CHAR_BIT ) & CC_MAP_HASH_FRAG_MASK; +} + +// Standard quadratic probing formula that guarantees that all buckets are visited when the bucket count is a power of +// two (at least in theory, because the displacement limit could terminate the search early when the bucket count is +// high). +static inline size_t cc_quadratic( uint16_t displacement ) +{ + return ( (size_t)displacement * displacement + displacement ) / 2; +} + +#define CC_MAP_MIN_NONZERO_BUCKET_COUNT 8 // Must be a power of two. + // Map header. typedef struct { alignas( max_align_t ) size_t size; - size_t cap; + size_t cap_mask; // Rather than storing the capacity (i.e. bucket count) directly, we store the bit mask used to + // reduce a hash code or displacement-derived bucket index to the buckets array, i.e. the capacity + // minus one. + // Consequently, a zero bucket count (i.e. a placeholder map) constitutes a special case, represented + // by all bits unset (i.e. zero). + uint16_t *metadata; // As described above, each metadatum consists of a 4-bit hash-code fragment (X), a 1-bit flag + // indicating whether the key in this bucket begins a chain associated with the bucket (Y), and + // an 11-bit value indicating the quadratic displacement of the next key in the chain (Z): + // XXXXYZZZZZZZZZZZ. + // The metadata array lives in the same allocation as the header and buckets array, but we + // store a pointer to it to avoid constantly recalculating its offset. } cc_map_hdr_ty; -// Placeholder for a map with no allocated memory. // In the case of maps, this placeholder allows us to avoid checking for a NULL handle inside functions. -static const cc_map_hdr_ty cc_map_placeholder = { 0, 0 }; +// Setting the placeholder's metadata pointer to point to a CC_MAP_EMPTY placeholder, rather than NULL, allows us to +// avoid checking for a zero bucket count during insertion and lookup. +static const uint16_t cc_map_placeholder_metadatum = CC_MAP_EMPTY; +static const cc_map_hdr_ty cc_map_placeholder = { 0, 0x0000000000000000ull, (uint16_t *)&cc_map_placeholder_metadatum }; -// Easy header access function. static inline cc_map_hdr_ty *cc_map_hdr( void *cntr ) { return (cc_map_hdr_ty *)cntr; @@ -1906,167 +2030,324 @@ static inline size_t cc_map_size( void *cntr ) static inline size_t cc_map_cap( void *cntr ) { - return cc_map_hdr( cntr )->cap; + // If the capacity is zero, cap_mask will be zero, not the capacity minus one. + // We account for this special case by adding (bool)cap_mask rather than one. + return cc_map_hdr( cntr )->cap_mask + (bool)cc_map_hdr( cntr )->cap_mask; } static inline bool cc_map_is_placeholder( void *cntr ) { - return cc_map_cap( cntr ) == 0; + return !cc_map_hdr( cntr )->cap_mask; } -// Functions for easily accessing element, key, and probe length for the bucket at index i. -// The element pointer also denotes the beginning of the bucket. - static inline void *cc_map_el( void *cntr, - size_t i, + size_t bucket, size_t el_size, uint64_t layout ) { - return (char *)cntr + sizeof( cc_map_hdr_ty ) + CC_BUCKET_SIZE( el_size, layout ) * i; + return (char *)cntr + sizeof( cc_map_hdr_ty ) + CC_BUCKET_SIZE( el_size, layout ) * bucket; } static inline void *cc_map_key( void *cntr, - size_t i, + size_t bucket, size_t el_size, uint64_t layout ) { - return (char *)cc_map_el( cntr, i, el_size, layout ) + CC_KEY_OFFSET( el_size, layout ); + return (char *)cc_map_el( cntr, bucket, el_size, layout ) + CC_KEY_OFFSET( el_size, layout ); } -static inline cc_probelen_ty *cc_map_probelen( - void *cntr, - size_t i, +static inline void *cc_map_key_for( + void *itr, size_t el_size, uint64_t layout ) { - return (cc_probelen_ty *)( (char *)cc_map_el( cntr, i, el_size, layout ) + CC_PROBELEN_OFFSET( el_size, layout ) ); + return (char *)itr + CC_KEY_OFFSET( el_size, layout ); +} + +static inline size_t cc_map_bucket_index_from_itr( void *cntr, void *itr, size_t el_size, uint64_t layout ) +{ + return ( (char *)itr - (char *)cc_map_el( cntr, 0, el_size, layout ) ) / CC_BUCKET_SIZE( el_size, layout ); +} + +static inline size_t cc_map_min_cap_for_n_els( + size_t n, + double max_load +) +{ + if( n == 0 ) + return 0; + + // Round up to a power of two. + size_t cap = CC_MAP_MIN_NONZERO_BUCKET_COUNT; + while( n > cap * max_load ) + cap *= 2; + + return cap; +} + +// Calculates the metadata array offset and total allocation size for a map with a given non-zero capacity. +// The data is organized in memory as follows: +// +--------+-----------------------------+-----+----------------+--------+ +// | Header | Buckets | Pad | Metadata | Excess | +// +--------+-----------------------------+-----+----------------+--------+ +// The metadata array requires four excess elements to ensure that iteration functions, which read four metadata at a +// time, never read beyond the end of it. +static inline void cc_map_allocation_details( + size_t cap, + size_t el_size, + uint64_t layout, + size_t *metadata_offset, + size_t *allocation_size +) +{ + size_t buckets_size = CC_BUCKET_SIZE( el_size, layout ) * cap; + *metadata_offset = sizeof( cc_map_hdr_ty ) + buckets_size + CC_PADDING( buckets_size, alignof( uint16_t ) ); + *allocation_size = *metadata_offset + sizeof( uint16_t ) * ( cap + 4 ); +} + +// Finds the earliest empty bucket in which a key-element pair belonging to home_bucket can be placed, assuming that +// home_bucket is already occupied. +// The reason to begin the search at home_bucket, rather than the end of the existing chain, is that key-element pairs +// deleted from other chains might have freed up buckets that could fall in this chain before the final key. +// Returns true if an empty bucket within the range of the displacement limit was found, in which case the final two +// pointer arguments contain the index of the empty bucket and its quadratic displacement from home_bucket. + +static inline bool cc_map_find_first_empty( + void *cntr, + size_t home_bucket, + size_t *empty, + uint16_t *displacement +) +{ + *displacement = 1; + size_t linear_dispacement = 1; + + while( true ) + { + *empty = ( home_bucket + linear_dispacement ) & cc_map_hdr( cntr )->cap_mask; + if( cc_map_hdr( cntr )->metadata[ *empty ] == CC_MAP_EMPTY ) + return true; + + if( CC_UNLIKELY( ++*displacement == CC_MAP_DISPLACEMENT_MASK ) ) + return false; + + linear_dispacement += *displacement; + } +} + +// Finds the key-element pair in the chain beginning in home_bucket after which to link a new key-element pair with +// displacement_to_empty quadratic displacement and returns the index of the bucket containing that key-element pair. +// Although the new key-element pair could simply be linked to the end of the chain, keeping the chain ordered by +// displacement theoretically improves cache locality during lookups. +static inline size_t cc_map_find_insert_location_in_chain( + void *cntr, + size_t home_bucket, + uint16_t displacement_to_empty +) +{ + size_t candidate = home_bucket; + while( true ) + { + uint16_t displacement = cc_map_hdr( cntr )->metadata[ candidate ] & CC_MAP_DISPLACEMENT_MASK; + + if( displacement > displacement_to_empty ) + return candidate; + + candidate = ( home_bucket + cc_quadratic( displacement ) ) & cc_map_hdr( cntr )->cap_mask; + } +} + +// Frees up a bucket occupied by a key-element pair not belonging there so that a new key-element pair belonging there +// can be placed there as the beginning of a new chain. +// This requires: +// - Finding the previous key-element pair in the chain to which the occupying key-element pair belongs by rehashing the +// key and traversing the chain. +// - Disconnecting the key-element pair from the chain. +// - Finding the appropriate empty bucket to which to move the key-element pair. +// - Moving the key-element pair to the empty bucket. +// - Re-linking the key-element pair to the chain. +// Returns true if the eviction succeeded, or false if no empty bucket to which to evict the occupying key-element pair +// could be found within the displacement limit. +static inline bool cc_map_evict( + void *cntr, + size_t bucket, + size_t el_size, + uint64_t layout, + cc_hash_fnptr_ty hash +) +{ + // Find the previous key-element pair in chain. + size_t home_bucket = hash( cc_map_key( cntr, bucket, el_size, layout ) ) & cc_map_hdr( cntr )->cap_mask; + size_t prev = home_bucket; + while( true ) + { + size_t next = ( home_bucket + cc_quadratic( cc_map_hdr( cntr )->metadata[ prev ] & CC_MAP_DISPLACEMENT_MASK ) ) & + cc_map_hdr( cntr )->cap_mask; + + if( next == bucket ) + break; + + prev = next; + } + + // Disconnect the key-element pair from chain. + cc_map_hdr( cntr )->metadata[ prev ] = ( cc_map_hdr( cntr )->metadata[ prev ] & ~CC_MAP_DISPLACEMENT_MASK ) | + ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK ); + + // Find the empty bucket to which to move the key-element pair. + size_t empty; + uint16_t displacement; + if( CC_UNLIKELY( !cc_map_find_first_empty( cntr, home_bucket, &empty, &displacement ) ) ) + return false; + + // Find the key-element pair in the chain after which to link the moved key-element pair. + prev = cc_map_find_insert_location_in_chain( cntr, home_bucket, displacement ); + + // Move the key and element. + memcpy( + cc_map_el( cntr, empty, el_size, layout ), + cc_map_el( cntr, bucket, el_size, layout ), + CC_BUCKET_SIZE( el_size, layout ) + ); + + // Re-link the key-element pair to the chain from its new bucket. + cc_map_hdr( cntr )->metadata[ empty ] = ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_HASH_FRAG_MASK ) | + ( cc_map_hdr( cntr )->metadata[ prev ] & CC_MAP_DISPLACEMENT_MASK ); + cc_map_hdr( cntr )->metadata[ prev ] = ( cc_map_hdr( cntr )->metadata[ prev ] & ~CC_MAP_DISPLACEMENT_MASK ) | + displacement; + + return true; } -// Inserts an element into the map. -// Assumes that the map has empty slots and therefore that failure cannot occur (hence the "raw" label). -// If replace is true, then el will replace any existing element with the same key. -// Returns a pointer-iterator to the newly inserted element, or to the existing element with the same key if replace is -// false. -// For the exact mechanics of Robin Hood hashing, see Sebastian Sylvan's helpful article: -// www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation -// However, this function includes an optimization not mentioned in descriptions of Robin Hood hashing. -// Traditionally, when attempting to place the pending element, if we reach a bucket containing an element with a lower -// probe length then we swap the pending element with that element and continue with the same loop. -// Here, upon reaching such a bucket (or any empty bucket), we simply identify the next empty bucket and then shift all -// elements from (and including) the former up to the latter forward one bucket. -// This approach has two advantages: -// - Once we have placed the original pending element, key comparison checks for subsequent buckets are superfluous, so -// a second, inner loop allows us to skip those checks. -// - Swapping a placed element with the pending element requires copying via an intermediate buffer, so the traditional -// approach copies every shifted element twice. -// Conversely, identifying the final empty bucket first and then shifting all elements in all intervening buckets at -// once only copies each shifted element once, albeit at the cost of copying padding bytes and possibly shifting -// elements that didn't actually need to be shifted. -// This approach performed marginally faster in benchmarking. +// Inserts a key-element pair, optionally replacing the existing key-element pair containing the same key if it exists. +// There are two main cases that must be handled: +// - If the key-element pair's home bucket is empty or occupied by a key-element pair that does not belong there, then +// the key-element pair is inserted there, evicting the occupying key-element pair if there is one. +// - Otherwise, the chain of key-element pairs beginning at the home bucket is traversed in search of a matching key. +// If none is found, then the new key-element pair is inserted at the earliest available bucket, per quadratic probing +// from the home bucket, and then linked to the chain in a manner that maintains its quadratic order. +// The replace argument tells the function whether to replace an existing key-element pair. +// If replace is true, the function returns a pointer-iterator to the inserted element, or NULL if the key-element pair +// was not inserted because of the max load factor or displacement limit constraints. +// If replace is false, then the return value is as described above, except that if the key already exists, the function +// returns a pointer-iterator to the associated element. static inline void *cc_map_insert_raw( void *cntr, void *el, void *key, - size_t key_hash, bool replace, size_t el_size, uint64_t layout, + double max_load, + cc_hash_fnptr_ty hash, cc_cmpr_fnptr_ty cmpr, cc_dtor_fnptr_ty el_dtor, cc_dtor_fnptr_ty key_dtor ) { - size_t i = key_hash & ( cc_map_hdr( cntr )->cap - 1 ); - cc_probelen_ty probelen = 1; - - while( true ) + size_t key_hash = hash( key ); + uint16_t hashfrag = cc_hash_frag( key_hash ); + size_t home_bucket = key_hash & cc_map_hdr( cntr )->cap_mask; + + // Case 1: The home bucket is empty or contains a key-element pair that doesn't belong there. + // This case also implicitly handles the case of a zero-capacity placeholder map, since home_bucket will be zero and + // metadata[ 0 ] will be the empty placeholder metadatum. + // In that scenario, the zero cap_mask triggers the below load-factor check. + if( !( cc_map_hdr( cntr )->metadata[ home_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK ) ) { - if( probelen > *cc_map_probelen( cntr, i, el_size, layout ) ) - { - // Empty bucket, or stealing an occupied bucket. - - // Find first empty bucket at or after i. - // Since the intervening elements will be shifted, we update their probe lengths at the same time. - size_t j = i; - while( true ) - { - if( !*cc_map_probelen( cntr, j, el_size, layout ) ) - break; + // Load-factor check. + if( CC_UNLIKELY( cc_map_hdr( cntr )->size + 1 > max_load * cc_map_cap( cntr ) ) ) + return NULL; - ++*cc_map_probelen( cntr, j, el_size, layout ); - j = ( j + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - } + // Vacate the home bucket if it contains a key-element pair. + if( + cc_map_hdr( cntr )->metadata[ home_bucket ] != CC_MAP_EMPTY && + CC_UNLIKELY( !cc_map_evict( cntr, home_bucket, el_size, layout, hash ) ) + ) + return NULL; - // Shift all elements [i to j) forward one bucket. + memcpy( cc_map_key( cntr, home_bucket, el_size, layout ), key, CC_KEY_SIZE( layout ) ); + memcpy( cc_map_el( cntr, home_bucket, el_size, layout ), el, el_size ); + cc_map_hdr( cntr )->metadata[ home_bucket ] = hashfrag | CC_MAP_IN_HOME_BUCKET_MASK | CC_MAP_DISPLACEMENT_MASK; - if( j < i ) - { - memmove( - cc_map_el( cntr, 1, el_size, layout ), - cc_map_el( cntr, 0, el_size, layout ), - j * CC_BUCKET_SIZE( el_size, layout ) - ); - - memcpy( - cc_map_el( cntr, 0, el_size, layout ), - cc_map_el( cntr, cc_map_hdr( cntr )->cap - 1, el_size, layout ), - CC_BUCKET_SIZE( el_size, layout ) - ); - - j = cc_map_hdr( cntr )->cap - 1; - } + ++cc_map_hdr( cntr )->size; - if( i != j ) - memmove( - cc_map_el( cntr, i + 1, el_size, layout ), - cc_map_el( cntr, i, el_size, layout ), - ( j - i ) * CC_BUCKET_SIZE( el_size, layout ) - ); + return cc_map_el( cntr, home_bucket, el_size, layout ); + } - // Insert the new element and key into the vacated bucket. - memcpy( cc_map_key( cntr, i, el_size, layout ), key, CC_KEY_SIZE( layout ) ); - memcpy( cc_map_el( cntr, i, el_size, layout ), el, el_size ); - *cc_map_probelen( cntr, i, el_size, layout ) = probelen; + // Case 2: The home bucket contains the beginning of a chain. - ++cc_map_hdr( cntr )->size; - return cc_map_el( cntr, i, el_size, layout ); - } - else if( - probelen == *cc_map_probelen( cntr, i, el_size, layout ) && - cmpr( cc_map_key( cntr, i, el_size, layout ), key ) + // Check the existing chain. + size_t bucket = home_bucket; + while( true ) + { + if( + ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_HASH_FRAG_MASK ) == hashfrag && + CC_LIKELY( cmpr( cc_map_key( cntr, bucket, el_size, layout ), key ) ) ) { - // Same key. - if( replace ) { if( key_dtor ) - key_dtor( cc_map_key( cntr, i, el_size, layout ) ); + key_dtor( cc_map_key( cntr, bucket, el_size, layout ) ); if( el_dtor ) - el_dtor( cc_map_el( cntr, i, el_size, layout ) ); + el_dtor( cc_map_el( cntr, bucket, el_size, layout ) ); - memcpy( cc_map_key( cntr, i, el_size, layout ), key, CC_KEY_SIZE( layout ) ); - memcpy( cc_map_el( cntr, i, el_size, layout ), el, el_size ); + memcpy( cc_map_key( cntr, bucket, el_size, layout ), key, CC_KEY_SIZE( layout ) ); + memcpy( cc_map_el( cntr, bucket, el_size, layout ), el, el_size ); } - return cc_map_el( cntr, i, el_size, layout ); + return cc_map_el( cntr, bucket, el_size, layout ); } - i = ( i + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - ++probelen; + uint16_t displacement = cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK; + if( displacement == CC_MAP_DISPLACEMENT_MASK ) + break; + + bucket = ( home_bucket + cc_quadratic( displacement ) ) & cc_map_hdr( cntr )->cap_mask; } + + // Load-factor check. + if( CC_UNLIKELY( cc_map_hdr( cntr )->size + 1 > max_load * cc_map_cap( cntr ) ) ) + return NULL; + + // Find the earliest empty bucket, per quadratic probing. + size_t empty; + uint16_t displacement; + if( CC_UNLIKELY( !cc_map_find_first_empty( cntr, home_bucket, &empty, &displacement ) ) ) + return NULL; + + size_t prev = cc_map_find_insert_location_in_chain( cntr, home_bucket, displacement ); + + // Insert the new key and element in the empty bucket and link it to the chain. + + memcpy( cc_map_key( cntr, empty, el_size, layout ), key, CC_KEY_SIZE( layout ) ); + memcpy( cc_map_el( cntr, empty, el_size, layout ), el, el_size ); + + cc_map_hdr( cntr )->metadata[ empty ] = hashfrag | ( cc_map_hdr( cntr )->metadata[ prev ] & CC_MAP_DISPLACEMENT_MASK + ); + cc_map_hdr( cntr )->metadata[ prev ] = ( cc_map_hdr( cntr )->metadata[ prev ] & ~CC_MAP_DISPLACEMENT_MASK ) | + displacement; + + ++cc_map_hdr( cntr )->size; + + return cc_map_el( cntr, empty, el_size, layout ); } -// Same as the previous function, except for elements with keys known not to already exist in the map. -// This function is used for rehashing when the map's capacity changes. -// When we know that the key is new, we can skip certain checks and achieve a small performance improvement. -static inline void *cc_map_insert_raw_unique( +// Inserts a key-element pair, assuming that the key does not already exist and that the map's capacity is large enough +// to accommodate it without violating the load factor constraint. +// These conditions are met during map resizing and rehashing. +// This function is the same as cc_map_insert_raw, except that no load-factor check or check of the existing chain is +// performed. +// It returns a pointer-iterator to the inserted element, or NULL if the key-element pair was not inserted because of +// the max load factor or displacement limit constraints. +static inline void *cc_map_reinsert( void *cntr, void *el, void *key, @@ -2075,117 +2356,117 @@ static inline void *cc_map_insert_raw_unique( cc_hash_fnptr_ty hash ) { - size_t i = hash( key ) & ( cc_map_hdr( cntr )->cap - 1 ); - cc_probelen_ty probelen = 1; + size_t key_hash = hash( key ); + uint16_t hashfrag = cc_hash_frag( key_hash ); + size_t home_bucket = key_hash & cc_map_hdr( cntr )->cap_mask; - while( true ) + if( !( cc_map_hdr( cntr )->metadata[ home_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK ) ) { - if( probelen > *cc_map_probelen( cntr, i, el_size, layout ) ) - { - // Empty bucket, or stealing an occupied bucket. - - size_t j = i; - while( true ) - { - if( !*cc_map_probelen( cntr, j, el_size, layout ) ) - break; + if( + cc_map_hdr( cntr )->metadata[ home_bucket ] != CC_MAP_EMPTY && + CC_UNLIKELY( !cc_map_evict( cntr, home_bucket, el_size, layout, hash ) ) + ) + return NULL; - ++*cc_map_probelen( cntr, j, el_size, layout ); - j = ( j + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - } + memcpy( cc_map_key( cntr, home_bucket, el_size, layout ), key, CC_KEY_SIZE( layout ) ); + memcpy( cc_map_el( cntr, home_bucket, el_size, layout ), el, el_size ); + cc_map_hdr( cntr )->metadata[ home_bucket ] = hashfrag | CC_MAP_IN_HOME_BUCKET_MASK | CC_MAP_DISPLACEMENT_MASK; - if( j < i ) - { - memmove( - cc_map_el( cntr, 1, el_size, layout ), - cc_map_el( cntr, 0, el_size, layout ), - j * CC_BUCKET_SIZE( el_size, layout ) - ); - - memcpy( - cc_map_el( cntr, 0, el_size, layout ), - cc_map_el( cntr, cc_map_hdr( cntr )->cap - 1, el_size, layout ), - CC_BUCKET_SIZE( el_size, layout ) - ); - - j = cc_map_hdr( cntr )->cap - 1; - } + ++cc_map_hdr( cntr )->size; - if( i != j ) - memmove( - cc_map_el( cntr, i + 1, el_size, layout ), - cc_map_el( cntr, i, el_size, layout ), - ( j - i ) * CC_BUCKET_SIZE( el_size, layout ) - ); + return cc_map_el( cntr, home_bucket, el_size, layout ); + } - memcpy( cc_map_key( cntr, i, el_size, layout ), key, CC_KEY_SIZE( layout ) ); - memcpy( cc_map_el( cntr, i, el_size, layout ), el, el_size ); - *cc_map_probelen( cntr, i, el_size, layout ) = probelen; + size_t empty; + uint16_t displacement; + if( CC_UNLIKELY( !cc_map_find_first_empty( cntr, home_bucket, &empty, &displacement ) ) ) + return NULL; - ++cc_map_hdr( cntr )->size; - return cc_map_el( cntr, i, el_size, layout ); - } + size_t prev = cc_map_find_insert_location_in_chain( cntr, home_bucket, displacement ); - i = ( i + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - ++probelen; - } -} + memcpy( cc_map_key( cntr, empty, el_size, layout ), key, CC_KEY_SIZE( layout ) ); + memcpy( cc_map_el( cntr, empty, el_size, layout ), el, el_size ); -// Returns the minimum capacity required to accommodate n elements, which is governed by the max load factor associated -// with the map's key type. -static inline size_t cc_map_min_cap_for_n_els( size_t n, double max_load ) -{ - if( n == 0 ) - return 0; + cc_map_hdr( cntr )->metadata[ empty ] = hashfrag | ( cc_map_hdr( cntr )->metadata[ prev ] & CC_MAP_DISPLACEMENT_MASK + ); + cc_map_hdr( cntr )->metadata[ prev ] = ( cc_map_hdr( cntr )->metadata[ prev ] & ~CC_MAP_DISPLACEMENT_MASK ) | + displacement; - // Round up to power of 2. - size_t cap = 8; - while( n > cap * max_load ) - cap *= 2; + ++cc_map_hdr( cntr )->size; - return cap; + return cc_map_el( cntr, empty, el_size, layout ); } // Creates a rehashed duplicate of cntr with capacity cap. -// Assumes that cap is large enough to accommodate all elements in cntr without violating the max load factor. +// Assumes that cap is a power of two large enough to accommodate all key-element pairs without violating the max load +// factor. // Returns a pointer to the duplicate, or NULL in the case of allocation failure. +// As this function is called very rarely in cc_map_insert and cc_map_get_or_insert, ideally it should not be inlined +// into those functions. +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wattributes" // Silence warning about combining noinline with static inline. +__attribute__((noinline)) +#endif static inline void *cc_map_make_rehash( void *cntr, size_t cap, size_t el_size, uint64_t layout, cc_hash_fnptr_ty hash, - cc_realloc_fnptr_ty realloc_ + cc_realloc_fnptr_ty realloc_, + cc_free_fnptr_ty free_ ) { - cc_map_hdr_ty *new_cntr = (cc_map_hdr_ty *)realloc_( - NULL, - sizeof( cc_map_hdr_ty ) + CC_BUCKET_SIZE( el_size, layout ) * cap - ); - if( !new_cntr ) - return NULL; + // The attempt to resize and rehash must occur inside a loop that incrementally doubles the target bucket count + // because a failure could theoretically occur at any load factor due to the displacement limit. + while( true ) + { + size_t metadata_offset; + size_t allocation_size; + cc_map_allocation_details( cap, el_size, layout, &metadata_offset, &allocation_size ); - new_cntr->size = 0; - new_cntr->cap = cap; - for( size_t i = 0; i < cap; ++i ) - *cc_map_probelen( new_cntr, i, el_size, layout ) = 0; - - for( size_t i = 0; i < cc_map_hdr( cntr )->cap; ++i ) - if( *cc_map_probelen( cntr, i, el_size, layout ) ) - cc_map_insert_raw_unique( - new_cntr, - cc_map_el( cntr, i, el_size, layout ), - cc_map_key( cntr, i, el_size, layout ), - el_size, - layout, - hash - ); + cc_map_hdr_ty *new_cntr = (cc_map_hdr_ty *)realloc_( NULL, allocation_size ); + if( CC_UNLIKELY( !new_cntr ) ) + return NULL; - return new_cntr; + new_cntr->size = 0; + new_cntr->cap_mask = cap - 1; + new_cntr->metadata = (uint16_t *)( (char *)new_cntr + metadata_offset ); + + memset( new_cntr->metadata, 0x00, ( cap + 4 ) * sizeof( uint16_t ) ); + + // Iteration stopper at the end of the actual metadata array (i.e. the first of the four excess metadata). + new_cntr->metadata[ cap ] = 0x01; + + for( size_t i = 0; i < cc_map_cap( cntr ); ++i ) + if( cc_map_hdr( cntr )->metadata[ i ] != CC_MAP_EMPTY ) + { + void *key = cc_map_key( cntr, i, el_size, layout ); + if( CC_UNLIKELY( !cc_map_reinsert( + new_cntr, + cc_map_el( cntr, i, el_size, layout ), + key, + el_size, + layout, + hash + ) ) ) + { + free_( new_cntr ); + cap *= 2; + continue; + } + } + + return new_cntr; + } } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif -// Reserves capacity such that the map can accommodate n elements without reallocation (i.e. without violating the -// max load factor). +// Reserves capacity such that the map can accommodate n key-element pairs without reallocation (i.e. without violating +// the max load factor). // Returns a cc_allocing_fn_result_ty containing the new container handle and a pointer that evaluates to true if the // operation was successful or false in the case of allocation failure. static inline cc_allocing_fn_result_ty cc_map_reserve( @@ -2210,9 +2491,10 @@ static inline cc_allocing_fn_result_ty cc_map_reserve( el_size, layout, hash, - realloc_ + realloc_, + free_ ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); if( !cc_map_is_placeholder( cntr ) ) @@ -2221,16 +2503,13 @@ static inline cc_allocing_fn_result_ty cc_map_reserve( return cc_make_allocing_fn_result( new_cntr, cc_dummy_true_ptr ); } -// Inserts an element. -// If replace is true, then el replaces any existing element with the same key. -// If the map exceeds its load factor, the underlying storage is expanded and a complete rehash occurs. -// Returns a cc_allocing_fn_result_ty containing the new container handle and a pointer to the newly inserted element, -// or to the existing element with the same key if replace is false. +// Inserts a key-element pair. +// If replace is true, then the new key-element pair replaces any existing key-element pair containing the same key. +// This function wraps cc_map_insert_raw in a loop that handles growing and rehashing the table if a new key-element +// pair cannot be inserted because of the max load factor or displacement limit constraints. +// Returns a cc_allocing_fn_result_ty containing the new container handle and a pointer-iterator to the newly inserted +// element, or to the existing element with the matching key if replace is false. // If the underlying storage needed to be expanded and an allocation failure occurred, the latter pointer will be NULL. -// This function checks to ensure that the map could accommodate an insertion before searching for the existing element. -// Therefore, failure can occur even if an element with the same key already exists and no reallocation was actually -// necessary. -// This was a design choice in favor of code simplicity and readability over ideal behavior in a corner case. static inline cc_allocing_fn_result_ty cc_map_insert( void *cntr, void *el, @@ -2247,42 +2526,45 @@ static inline cc_allocing_fn_result_ty cc_map_insert( cc_free_fnptr_ty free_ ) { - if( cc_map_size( cntr ) + 1 > cc_map_cap( cntr ) * max_load ) + while( true ) { - cc_allocing_fn_result_ty result = cc_map_reserve( + void *itr = cc_map_insert_raw( cntr, - cc_map_size( cntr ) + 1, + el, + key, + replace, el_size, layout, - hash, max_load, + hash, + cmpr, + el_dtor, + key_dtor + ); + + if( CC_LIKELY( itr ) ) + return cc_make_allocing_fn_result( cntr, itr ); + + void *new_cntr = cc_map_make_rehash( + cntr, + cc_map_hdr( cntr )->cap_mask ? cc_map_cap( cntr ) * 2 : CC_MAP_MIN_NONZERO_BUCKET_COUNT, + el_size, + layout, + hash, realloc_, free_ ); - if( !result.other_ptr ) - return result; - - cntr = result.new_cntr; - } + if( CC_UNLIKELY( !new_cntr ) ) + return cc_make_allocing_fn_result( cntr, NULL ); - void *new_el = cc_map_insert_raw( - cntr, - el, - key, - hash( key ), - replace, - el_size, - layout, - cmpr, - el_dtor, - key_dtor - ); + if( !cc_map_is_placeholder( cntr ) ) + free_( cntr ); - return cc_make_allocing_fn_result( cntr, new_el ); + cntr = new_cntr; + } } -// Returns a pointer-iterator to the element with the specified key, or NULL if no such element exists. static inline void *cc_map_get( void *cntr, void *key, @@ -2292,86 +2574,275 @@ static inline void *cc_map_get( cc_cmpr_fnptr_ty cmpr ) { - if( cc_map_size( cntr ) == 0 ) - return NULL; + size_t key_hash = hash( key ); + size_t home_bucket = key_hash & cc_map_hdr( cntr )->cap_mask; - size_t i = hash( key ) & ( cc_map_hdr( cntr )->cap - 1 ); - cc_probelen_ty probelen = 1; + // If the home bucket is empty or contains a key-element pair that does not belong there, then our key does not exist. + // This check also implicitly handles the case of a zero bucket count, since home_bucket will be zero and + // metadata[ 0 ] will be the empty placeholder. + if( !( cc_map_hdr( cntr )->metadata[ home_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK ) ) + return NULL; - while( probelen <= *cc_map_probelen( cntr, i, el_size, layout ) ) + // Traverse the chain of key-element pairs belonging to the home bucket. + uint16_t hashfrag = cc_hash_frag( key_hash ); + size_t bucket = home_bucket; + while( true ) { if( - probelen == *cc_map_probelen( cntr, i, el_size, layout ) && - cmpr( cc_map_key( cntr, i, el_size, layout ), key ) + ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_HASH_FRAG_MASK ) == hashfrag && + CC_LIKELY( cmpr( cc_map_key( cntr, bucket, el_size, layout ), key ) ) ) - return cc_map_el( cntr, i, el_size, layout ); + return cc_map_el( cntr, bucket, el_size, layout ); + + uint16_t displacement = cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK; + if( displacement == CC_MAP_DISPLACEMENT_MASK ) + return NULL; - i = ( i + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - ++probelen; + bucket = ( home_bucket + cc_quadratic( displacement ) ) & cc_map_hdr( cntr )->cap_mask; } +} - return NULL; +// For maps, the container handle doubles up as r_end. +static inline void *cc_map_r_end( void *cntr ) +{ + return cntr; } -// Returns a pointer to the key for the element pointed to by the specified pointer-iterator. -static inline void *cc_map_key_for( +static inline void *cc_map_end( + void *cntr, + size_t el_size, + uint64_t layout +) +{ + return cc_map_el( cntr, cc_map_cap( cntr ), el_size, layout ); +} + +// Finds the first occupied bucket at or after the bucket pointed to by itr. +// This function scans four buckets at a time, ideally using intrinsics. +static inline void *cc_map_leap_forward( void *cntr, void *itr, size_t el_size, uint64_t layout ) +{ + uint16_t *itr_metadatum = cc_map_hdr( cntr )->metadata + cc_map_bucket_index_from_itr( cntr, itr, el_size, layout ); + + while( true ) + { + uint64_t metadata; + memcpy( &metadata, itr_metadatum, sizeof( uint64_t ) ); + if( metadata ) + return (char *)itr + CC_BUCKET_SIZE( el_size, layout ) * cc_first_nonzero_uint16( metadata ); + + itr = (char *)itr + CC_BUCKET_SIZE( el_size, layout ) * 4; + itr_metadatum += 4; + } +} + +// Finds the first occupied bucket before the bucket pointed to by itr. +// This function also scans four buckets at a time, ideally using intrinsics. +// However, because the r_end pointer-iterator, unlike end, constitutes a special case, this function is less efficient +// cc_map_leap_forward. +static inline void *cc_map_leap_backward( void *cntr, void *itr, size_t el_size, uint64_t layout ) +{ + size_t bucket = cc_map_bucket_index_from_itr( cntr, itr, el_size, layout ); + + while( true ) + { + if( bucket < 4 ) + { + while( bucket-- ) + if( cc_map_hdr( cntr )->metadata[ bucket ] ) + return cc_map_el( cntr, bucket, el_size, layout ); + + return cc_map_r_end( cntr ); + } + + if( cc_map_hdr( cntr )->metadata == &cc_map_placeholder_metadatum ) + __builtin_unreachable(); + + uint64_t metadatum; + memcpy( &metadatum, cc_map_hdr( cntr )->metadata + bucket - 4, sizeof( uint64_t ) ); + if( metadatum ) + return cc_map_el( cntr, bucket - 1 - cc_last_nonzero_uint16( metadatum ), el_size, layout ); + + bucket -= 4; + } +} + +static inline void *cc_map_first( + void *cntr, + size_t el_size, + uint64_t layout +) +{ + void *itr = cc_map_el( cntr, 0, el_size, layout ); + + if( !cc_map_hdr( cntr )->cap_mask ) + return itr; + + return cc_map_leap_forward( cntr, itr, el_size, layout ); +} + +static inline void *cc_map_last( + void *cntr, + size_t el_size, + uint64_t layout +) +{ + return cc_map_leap_backward( cntr, cc_map_end( cntr, el_size, layout ), el_size, layout ); +} + +static inline void *cc_map_prev( + void *cntr, void *itr, size_t el_size, uint64_t layout ) { - return (char *)itr + CC_KEY_OFFSET( el_size, layout ); + return cc_map_leap_backward( cntr, itr, el_size, layout ); } -// Erases the element pointer to by pointer-iterator itr. -// For the exact mechanics of erasing elements in a Robin Hood hash table, see Sebastian Sylvan's: -// www.sebastiansylvan.com/post/more-on-robin-hood-hashing-2/ -static inline void cc_map_erase_itr( +static inline void *cc_map_next( void *cntr, void *itr, size_t el_size, - uint64_t layout, + uint64_t layout +) +{ + itr = (char *)itr + CC_BUCKET_SIZE( el_size, layout ); + return cc_map_leap_forward( cntr, itr, el_size, layout ); +} + +// Erases the key-element pair in the specified bucket. +// The erasure always occurs at the end of the chain to which the key-element pair belongs. +// If the key-element pair to be erased is not the last in the chain, it is swapped with the last so that erasure occurs +// at the end. +// This helps keep a chain's key-element pairs close to their home bucket for the sake of cache locality. +// Returns true if, in the case of iteration from first to end, cc_map_next should now be called on the pointer-iterator +// to find the next key-element pair. +// This return value is necessary because at the iterator location, the erasure could result in an empty bucket, a +// bucket containing a moved key-element pair already visited during the iteration, or a bucket containing a moved +// key-element pair not yet visited. +static inline bool cc_map_erase_raw( + void *cntr, + size_t erase_bucket, + size_t home_bucket, // SIZE_MAX if unknown. + size_t el_size, + size_t layout, + cc_hash_fnptr_ty hash, cc_dtor_fnptr_ty el_dtor, cc_dtor_fnptr_ty key_dtor ) { - size_t i = (size_t)( (char *)itr - (char *)cc_map_el( cntr, 0, el_size, layout ) ) / - CC_BUCKET_SIZE( el_size, layout ); - - *cc_map_probelen( cntr, i, el_size, layout ) = 0; --cc_map_hdr( cntr )->size; - if( key_dtor ) - key_dtor( cc_map_key( cntr, i, el_size, layout ) ); + // Case 1: The key-element pair is the only one in its chain, so just remove it. + if( + cc_map_hdr( cntr )->metadata[ erase_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK && + ( cc_map_hdr( cntr )->metadata[ erase_bucket ] & CC_MAP_DISPLACEMENT_MASK ) == CC_MAP_DISPLACEMENT_MASK + ) + { + if( el_dtor ) + el_dtor( cc_map_el( cntr, erase_bucket, el_size, layout ) ); + if( key_dtor ) + key_dtor( cc_map_key( cntr, erase_bucket, el_size, layout ) ); + + cc_map_hdr( cntr )->metadata[ erase_bucket ] = CC_MAP_EMPTY; + return true; + } + + // Case 2 and 3 require that we know the key-element pair's home bucket. + if( home_bucket == SIZE_MAX ) + { + if( cc_map_hdr( cntr )->metadata[ erase_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK ) + home_bucket = erase_bucket; + else + home_bucket = hash( cc_map_key( cntr, erase_bucket, el_size, layout ) ) & cc_map_hdr( cntr )->cap_mask; + } if( el_dtor ) - el_dtor( cc_map_el( cntr, i, el_size, layout ) ); + el_dtor( cc_map_el( cntr, erase_bucket, el_size, layout ) ); + if( key_dtor ) + key_dtor( cc_map_key( cntr, erase_bucket, el_size, layout ) ); + + // Case 2: The key-element pair is the last in a chain containing multiple key-element pairs. + // Traverse the chain from the beginning and find the penultimate key-element pair. + // Then disconnect the key-element pair and erase. + if( ( cc_map_hdr( cntr )->metadata[ erase_bucket ] & CC_MAP_DISPLACEMENT_MASK ) == CC_MAP_DISPLACEMENT_MASK ) + { + size_t bucket = home_bucket; + while( true ) + { + uint16_t displacement = cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK; + size_t next = ( home_bucket + cc_quadratic( displacement ) ) & cc_map_hdr( cntr )->cap_mask; + if( next == erase_bucket ) + { + cc_map_hdr( cntr )->metadata[ bucket ] |= CC_MAP_DISPLACEMENT_MASK; + cc_map_hdr( cntr )->metadata[ erase_bucket ] = CC_MAP_EMPTY; + return true; + } + bucket = next; + } + } + + // Case 3: The chain has multiple key-element pairs, and the key-element pair is not the last one. + // Traverse the chain from the key-element pair to be erased and find the last and penultimate key-element pairs. + // Disconnect the last key-element pair from the chain, and swap it with the key-element pair to erase. + size_t bucket = erase_bucket; while( true ) { - size_t next = ( i + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - if( *cc_map_probelen( cntr, next, el_size, layout ) <= 1 ) - break; // Empty slot or key already in its home bucket, so all done. - - //Bump backwards. + size_t prev = bucket; + bucket = ( home_bucket + cc_quadratic( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK ) ) & + cc_map_hdr( cntr )->cap_mask; - memcpy( - cc_map_key( cntr, i, el_size, layout ), - cc_map_key( cntr, next, el_size, layout ), - CC_KEY_SIZE( layout ) - ); - memcpy( cc_map_el( cntr, i, el_size, layout ), cc_map_el( cntr, next, el_size, layout ), el_size ); + if( ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK ) == CC_MAP_DISPLACEMENT_MASK ) + { + memcpy( + cc_map_el( cntr, erase_bucket, el_size, layout ), + cc_map_el( cntr, bucket, el_size, layout ), + CC_BUCKET_SIZE( el_size, layout ) + ); + + cc_map_hdr( cntr )->metadata[ erase_bucket ] = ( cc_map_hdr( cntr )->metadata[ erase_bucket ] & + ~CC_MAP_HASH_FRAG_MASK ) | ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_HASH_FRAG_MASK ); - *cc_map_probelen( cntr, i, el_size, layout ) = - *cc_map_probelen( cntr, next, el_size, layout ) - 1; - *cc_map_probelen( cntr, next, el_size, layout ) = 0; + cc_map_hdr( cntr )->metadata[ prev ] |= CC_MAP_DISPLACEMENT_MASK; + cc_map_hdr( cntr )->metadata[ bucket ] = CC_MAP_EMPTY; - i = next; + // Whether an pointer-iterator pointing to erase_bucket should be advanced depends on whether the key-element pair + // moved to the erase_bucket came from before or after that bucket. + // In the former case, the iteration would already have hit the moved key-element pair, so the pointer-iterator + // should still be advanced. + if( bucket > erase_bucket ) + return false; + + return true; + } } } -// Erases the element with the specified key, if it exists. -// Returns a pointer that evaluates to true if an element was erased, or else is NULL. +// Erases the key-element pair pointed to by itr and returns a pointer-iterator to the next key-element pair in the +// table. +// This function must be inlined to ensure that the compiler optimizes away the cc_map_next call if the returned +// pointer-iterator is discarded. +static inline CC_ALWAYS_INLINE void *cc_map_erase_itr( + void *cntr, + void *itr, + size_t el_size, + uint64_t layout, + cc_hash_fnptr_ty hash, + cc_dtor_fnptr_ty el_dtor, + cc_dtor_fnptr_ty key_dtor +) +{ + size_t bucket = cc_map_bucket_index_from_itr( cntr, itr, el_size, layout ); + + if( cc_map_erase_raw( cntr, bucket, SIZE_MAX, el_size, layout, hash, el_dtor, key_dtor ) ) + return cc_map_next( cntr, itr, el_size, layout ); + + return itr; +} + +// Erases the key-element pair containing the specified key, if it exists. +// Returns a pointer that evaluates to true if a key-element pair was erased, or else is NULL. // This pointer is eventually cast to bool by the cc_erase API macro. static inline void *cc_map_erase( void *cntr, @@ -2382,39 +2853,34 @@ static inline void *cc_map_erase( cc_cmpr_fnptr_ty cmpr, cc_dtor_fnptr_ty el_dtor, cc_dtor_fnptr_ty key_dtor, - cc_free_fnptr_ty free_ + CC_UNUSED( cc_free_fnptr_ty, free_ ) ) { - if( cc_map_size( cntr ) == 0 ) - return NULL; + size_t key_hash = hash( key ); + size_t home_bucket = key_hash & cc_map_hdr( cntr )->cap_mask; - size_t i = hash( key ) & ( cc_map_hdr( cntr )->cap - 1 ); - cc_probelen_ty probelen = 1; + if( !( cc_map_hdr( cntr )->metadata[ home_bucket ] & CC_MAP_IN_HOME_BUCKET_MASK ) ) + return NULL; - while( probelen <= *cc_map_probelen( cntr, i, el_size, layout ) ) + uint16_t hashfrag = cc_hash_frag( key_hash ); + size_t bucket = home_bucket; + while( true ) { if( - probelen == *cc_map_probelen( cntr, i, el_size, layout ) && - cmpr( cc_map_key( cntr, i, el_size, layout ), key ) + ( cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_HASH_FRAG_MASK ) == hashfrag && + CC_LIKELY( cmpr( cc_map_key( cntr, bucket, el_size, layout ), key ) ) ) { - cc_map_erase_itr( - cntr, - cc_map_el( cntr, i, el_size, layout ), - el_size, - layout, - el_dtor, - key_dtor - ); - - return cc_dummy_true_ptr; + cc_map_erase_raw( cntr, bucket, home_bucket, el_size, layout, hash, el_dtor, key_dtor ); + return &cc_dummy_true; } - i = ( i + 1 ) & ( cc_map_hdr( cntr )->cap - 1 ); - ++probelen; - } + uint16_t displacement = cc_map_hdr( cntr )->metadata[ bucket ] & CC_MAP_DISPLACEMENT_MASK; + if( displacement == CC_MAP_DISPLACEMENT_MASK ) + return NULL; - return NULL; + bucket = ( home_bucket + cc_quadratic( displacement ) ) & cc_map_hdr( cntr )->cap_mask; + } } // Shrinks the map's capacity to the minimum possible without violating the max load factor associated with the key @@ -2451,9 +2917,10 @@ static inline cc_allocing_fn_result_ty cc_map_shrink( el_size, layout, hash, - realloc_ + realloc_, + free_ ); - if( !new_cntr ) + if( CC_UNLIKELY( !new_cntr ) ) return cc_make_allocing_fn_result( cntr, NULL ); if( !cc_map_is_placeholder( cntr ) ) @@ -2467,7 +2934,7 @@ static inline cc_allocing_fn_result_ty cc_map_shrink( // the copy is a placeholder. // Hence, this function does no rehashing. // Returns a pointer to the copy, or NULL in the case of allocation failure. -// That return value is cast to bool in the corresponding macro. +// That return value is cast to bool in the corresponding API macro. static inline void *cc_map_init_clone( void *src, size_t el_size, @@ -2479,19 +2946,22 @@ static inline void *cc_map_init_clone( if( cc_map_size( src ) == 0 ) // Also handles placeholder. return (void *)&cc_map_placeholder; - cc_map_hdr_ty *new_cntr = (cc_map_hdr_ty*)realloc_( - NULL, - sizeof( cc_map_hdr_ty ) + CC_BUCKET_SIZE( el_size, layout ) * cc_map_cap( src ) - ); - if( !new_cntr ) + size_t buckets_size = CC_BUCKET_SIZE( el_size, layout ) * cc_map_cap( src ); + size_t metadata_offset = sizeof( cc_map_hdr_ty ) + buckets_size + CC_PADDING( buckets_size, alignof( uint16_t ) ); + size_t allocation_size = metadata_offset + sizeof( uint16_t ) * ( cc_map_cap( src ) + 4 ); + + cc_map_hdr_ty *new_cntr = (cc_map_hdr_ty*)realloc_( NULL, allocation_size ); + if( CC_UNLIKELY( !new_cntr ) ) return NULL; - memcpy( new_cntr, src, sizeof( cc_map_hdr_ty ) + CC_BUCKET_SIZE( el_size, layout ) * cc_map_cap( src ) ); + memcpy( new_cntr, src, allocation_size ); + new_cntr->metadata = (uint16_t *)( (char *)new_cntr + metadata_offset ); + return new_cntr; } -// Erases all elements, calling the destructors for the key and element types if necessary, without changing the map's -// capacity. +// Erases all key-element pairs, calling the destructors for the key and element types if necessary, without changing +// the map's capacity. static inline void cc_map_clear( void *cntr, size_t el_size, @@ -2504,16 +2974,16 @@ static inline void cc_map_clear( if( cc_map_size( cntr ) == 0 ) // Also handles placeholder. return; - for( size_t i = 0; i < cc_map_hdr( cntr )->cap; ++i ) - if( *cc_map_probelen( cntr, i, el_size, layout ) ) + for( size_t bucket = 0; bucket < cc_map_cap( cntr ); ++bucket ) + if( cc_map_hdr( cntr )->metadata[ bucket ] ) { if( key_dtor ) - key_dtor( cc_map_key( cntr, i, el_size, layout ) ); + key_dtor( cc_map_key( cntr, bucket, el_size, layout ) ); if( el_dtor ) - el_dtor( cc_map_el( cntr, i, el_size, layout ) ); + el_dtor( cc_map_el( cntr, bucket, el_size, layout ) ); - *cc_map_probelen( cntr, i, el_size, layout ) = 0; + cc_map_hdr( cntr )->metadata[ bucket ] = CC_MAP_EMPTY; } cc_map_hdr( cntr )->size = 0; @@ -2535,94 +3005,15 @@ static inline void cc_map_cleanup( free_( cntr ); } -// For maps, the container handle doubles up as r_end. -static inline void *cc_map_r_end( void *cntr ) -{ - return cntr; -} - -// Returns a pointer-iterator to the end of the buckets array. -static inline void *cc_map_end( - void *cntr, - size_t el_size, - uint64_t layout -) -{ - return cc_map_el( cntr, cc_map_hdr( cntr )->cap, el_size, layout ); -} - -// Returns a pointer-iterator to the first element, or end if the map is empty. -static inline void *cc_map_first( - void *cntr, - size_t el_size, - uint64_t layout -) -{ - for( size_t i = 0; i < cc_map_hdr( cntr )->cap; ++i ) - if( *cc_map_probelen( cntr, i, el_size, layout ) ) - return cc_map_el( cntr, i, el_size, layout ); - - return cc_map_end( cntr, el_size, layout ); -} - -// Returns a pointer-iterator to the last element, or r_end if the map is empty. -static inline void *cc_map_last( - void *cntr, - size_t el_size, - uint64_t layout -) -{ - for( size_t i = cc_map_hdr( cntr )->cap; i-- > 0; ) - if( *cc_map_probelen( cntr, i, el_size, layout ) ) - return cc_map_el( cntr, i, el_size, layout ); - - return cc_map_r_end( cntr ); -} - -static inline void *cc_map_prev( - void *cntr, - void *itr, - size_t el_size, - uint64_t layout -) -{ - while( true ) - { - if( itr == cc_map_el( cntr, 0, el_size, layout ) ) - return cc_map_r_end( cntr ); - - itr = (void *)( (char *)itr - CC_BUCKET_SIZE( el_size, layout ) ); - if( *(cc_probelen_ty *)( (char *)itr + CC_PROBELEN_OFFSET( el_size, layout ) ) ) - return itr; - } -} - -static inline void *cc_map_next( - void *cntr, - void *itr, - size_t el_size, - uint64_t layout -) -{ - do - itr = (void *)( (char *)itr + CC_BUCKET_SIZE( el_size, layout ) ); - while( - itr != cc_map_end( cntr, el_size, layout ) && - !*(cc_probelen_ty *)( (char *)itr + CC_PROBELEN_OFFSET( el_size, layout ) ) - ); - - return itr; -} - /*--------------------------------------------------------------------------------------------------------------------*/ /* Set */ /*--------------------------------------------------------------------------------------------------------------------*/ // A set is implemented as a map where the key and element are combined into one space in memory. // Hence, it reuses the functions for map, except: -// - The key offset inside the bucket is zero. -// This is handled at the API-macro level via the cc_layout function and associated macros. -// - The element size passed into map functions is zero in order to avoid double memcpy-ing. +// - The key offset inside the bucket is zero. +// This is handled at the API-macro level via the cc_layout function and associated macros. +// - The element size passed into map functions is zero in order to avoid double memcpy-ing. static inline size_t cc_set_size( void *cntr ) { @@ -2690,16 +3081,25 @@ static inline void *cc_set_get( return cc_map_get( cntr, key, 0 /* Zero element size */, layout, hash, cmpr ); } -static inline void cc_set_erase_itr( +static inline void *cc_set_erase_itr( void *cntr, void *itr, CC_UNUSED( size_t, el_size ), uint64_t layout, + cc_hash_fnptr_ty hash, cc_dtor_fnptr_ty el_dtor, CC_UNUSED( cc_dtor_fnptr_ty, key_dtor ) ) { - cc_map_erase_itr( cntr, itr, 0 /* Zero element size */, layout, el_dtor, NULL /* Only one destructor */ ); + return cc_map_erase_itr( + cntr, + itr, + 0, // Zero element size. + layout, + hash, + el_dtor, + NULL // Only one destructor. + ); } static inline void *cc_set_erase( @@ -3175,9 +3575,10 @@ static inline void *cc_set_next( /* Function arguments */ \ ( \ *(cntr), \ - itr, \ + (itr), \ CC_EL_SIZE( *(cntr) ), \ CC_LAYOUT( *(cntr) ), \ + CC_KEY_HASH( *(cntr) ), \ CC_EL_DTOR( *(cntr) ), \ CC_KEY_DTOR( *(cntr) ) \ ) \ @@ -3190,21 +3591,21 @@ static inline void *cc_set_next( CC_STATIC_ASSERT( CC_IS_SAME_TY( (cntr), (src) ) ), \ CC_POINT_HNDL_TO_ALLOCING_FN_RESULT( \ *(cntr), \ - cc_list_splice( *(cntr), itr, *(src), src_itr, CC_REALLOC_FN ) \ + cc_list_splice( *(cntr), (itr), *(src), src_itr, CC_REALLOC_FN ) \ ), \ CC_CAST_MAYBE_UNUSED( bool, CC_FIX_HNDL_AND_RETURN_OTHER_PTR( *(cntr) ) ) \ ) \ -#define cc_resize( cntr, n ) \ -( \ - CC_WARN_DUPLICATE_SIDE_EFFECTS( cntr ), \ - CC_STATIC_ASSERT( CC_CNTR_ID( *(cntr) ) == CC_VEC ), \ - CC_POINT_HNDL_TO_ALLOCING_FN_RESULT( \ - *(cntr), \ - cc_vec_resize( *(cntr), n, CC_EL_SIZE( *(cntr) ), CC_EL_DTOR( *(cntr) ), CC_REALLOC_FN ) \ - ), \ - CC_CAST_MAYBE_UNUSED( bool, CC_FIX_HNDL_AND_RETURN_OTHER_PTR( *(cntr) ) ) \ -) \ +#define cc_resize( cntr, n ) \ +( \ + CC_WARN_DUPLICATE_SIDE_EFFECTS( cntr ), \ + CC_STATIC_ASSERT( CC_CNTR_ID( *(cntr) ) == CC_VEC ), \ + CC_POINT_HNDL_TO_ALLOCING_FN_RESULT( \ + *(cntr), \ + cc_vec_resize( *(cntr), (n), CC_EL_SIZE( *(cntr) ), CC_EL_DTOR( *(cntr) ), CC_REALLOC_FN ) \ + ), \ + CC_CAST_MAYBE_UNUSED( bool, CC_FIX_HNDL_AND_RETURN_OTHER_PTR( *(cntr) ) ) \ +) \ #define cc_shrink( cntr ) \ ( \ @@ -3904,11 +4305,11 @@ cc_layout( CC_CNTR_ID( cntr ), CC_EL_SIZE( cntr ), alignof( CC_EL_TY( cntr ) ), // Integer types. -static inline size_t hash_uint64( uint64_t val ) +static inline size_t cc_hash_uint64( uint64_t val ) { #if SIZE_MAX == 0xFFFFFFFFFFFFFFFF || SIZE_MAX == 0xFFFFFFFF // 64-bit or 32-bit size_t. - // Fast-hash: https://jonkagstrom.com/bit-mixer-construction/ - // https://code.google.com/archive/p/fast-hash/ + // Fast-hash: https://jonkagstrom.com/bit-mixer-construction + // https://code.google.com/archive/p/fast-hash val ^= val >> 23; val *= 0x2127599BF4325C37ULL; val ^= val >> 47; @@ -3950,7 +4351,7 @@ static inline CC_ALWAYS_INLINE cc_cmpr_fnptr_ty cc_cmpr_##name##_select( size_t \ static inline size_t cc_hash_##name( void *void_val ) \ { \ - return hash_uint64( (uint64_t)*(ty *)void_val ); \ + return cc_hash_uint64( (uint64_t)*(ty *)void_val ); \ } \ CC_DEFAULT_INTEGER_CMPR_HASH_FUNCTIONS( char, char ) diff --git a/tests/tests_against_stl.cpp b/tests/tests_against_stl.cpp index 5e61dfb..fe70ad0 100644 --- a/tests/tests_against_stl.cpp +++ b/tests/tests_against_stl.cpp @@ -1,5 +1,6 @@ /* -CC/tests/tests_against_stl.cpp - v. 1.0.4 + +CC/tests/tests_against_stl.cpp - v1.1.0 This file tests CC containers against equivalent C++ STL containers. Primarily, it checks that a CC container and its equivalent STL container end up in the same state after a random @@ -24,6 +25,7 @@ License (MIT): WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #include diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 2d32126..50de407 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,5 +1,6 @@ /* -CC/tests/unit_tests.c - v1.0.4 + +CC/tests/unit_tests.c - v1.1.0 This file tests CC containers. It aims to cover the full API and to check corner cases, particularly transitions between placeholder containers and @@ -21,6 +22,7 @@ License (MIT): WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ #define TEST_VEC @@ -89,7 +91,7 @@ void tracking_free( void *ptr ) // Define a custom type that will be used to check that destructors are always called where necessary. bool dtor_called[ 100 ]; -void check_dtors_arr() +void check_dtors_arr( void ) { for( size_t i = 0; i < sizeof( dtor_called ) / sizeof( *dtor_called ); ++i ) { @@ -1204,29 +1206,60 @@ void test_map_erase_itr( void ) map( int, size_t ) our_map; init( &our_map ); - // Test erase existing. - for( int i = 0; i < 100; ++i ) + // In this instance, the key count and order of insert have been carefully chosen to cause skipped or repeat-visted + // keys if erase_itr does not correctly handle the case of another key being moved to the bucket of the erased key. + for( int i = 119; i >= 0; --i ) UNTIL_SUCCESS( insert( &our_map, i, i + 1 ) ); - ALWAYS_ASSERT( size( &our_map ) == 100 ); + ALWAYS_ASSERT( size( &our_map ) == 120 ); - for( int i = 0; i < 100; i += 2 ) + // Test with iterator from get. + for( uint64_t i = 0; i < 120; i += 4 ) erase_itr( &our_map, get( &our_map, i ) ); // Check. - ALWAYS_ASSERT( size( &our_map ) == 50 ); - for( int i = 0; i < 100; ++i ) + ALWAYS_ASSERT( size( &our_map ) == 90 ); + for( uint64_t i = 0; i < 120; ++i ) + { + if( i % 4 == 0 ) + ALWAYS_ASSERT( !get( &our_map, i ) ); + else + { + size_t *el = get( &our_map, i ); + ALWAYS_ASSERT( el && *el == i + 1 ); + } + } + + // Test deletion while iterating. + size_t *el = first( &our_map ); + size_t n_iterations = 0; + while( el != end( &our_map ) ) + { + ++n_iterations; + + if( *key_for( &our_map, el ) % 2 == 0 ) + el = erase_itr( &our_map, el ); + else + el = next( &our_map, el ); + } + + ALWAYS_ASSERT( n_iterations == 90 ); + ALWAYS_ASSERT( size( &our_map ) == 60 ); + + for( uint64_t i = 0; i < 120; ++i ) { if( i % 2 == 0 ) ALWAYS_ASSERT( !get( &our_map, i ) ); else - ALWAYS_ASSERT( *get( &our_map, i ) == (size_t)i + 1 ); + { + el = get( &our_map, i ); + ALWAYS_ASSERT( el && *el == i + 1 ); + } } cleanup( &our_map ); } - void test_map_clear( void ) { map( int, size_t ) our_map; @@ -1723,23 +1756,55 @@ void test_set_erase_itr( void ) set( int ) our_set; init( &our_set ); - // Test erase existing. - for( int i = 0; i < 100; ++i ) + // In this instance, the key count and order of insert have been carefully chosen to cause skipped or repeat-visted + // keys if vt_erase_itr does not correctly handle the case of another key being moved to the bucket of the erased key. + for( int i = 119; i >= 0; --i ) UNTIL_SUCCESS( insert( &our_set, i ) ); - ALWAYS_ASSERT( size( &our_set ) == 100 ); + ALWAYS_ASSERT( size( &our_set ) == 120 ); - for( int i = 0; i < 100; i += 2 ) + // Test with iterator from get. + for( uint64_t i = 0; i < 120; i += 4 ) erase_itr( &our_set, get( &our_set, i ) ); // Check. - ALWAYS_ASSERT( size( &our_set ) == 50 ); - for( int i = 0; i < 100; ++i ) + ALWAYS_ASSERT( size( &our_set ) == 90 ); + for( uint64_t i = 0; i < 120; ++i ) + { + if( i % 4 == 0 ) + ALWAYS_ASSERT( !get( &our_set, i ) ); + else + { + int *el = get( &our_set, i ); + ALWAYS_ASSERT( el && *el == i ); + } + } + + // Test deletion while iterating. + int *el = first( &our_set ); + size_t n_iterations = 0; + while( el != end( &our_set ) ) + { + ++n_iterations; + + if( *el % 2 == 0 ) + el = erase_itr( &our_set, el ); + else + el = next( &our_set, el ); + } + + ALWAYS_ASSERT( n_iterations == 90 ); + ALWAYS_ASSERT( size( &our_set ) == 60 ); + + for( uint64_t i = 0; i < 120; ++i ) { if( i % 2 == 0 ) ALWAYS_ASSERT( !get( &our_set, i ) ); else - ALWAYS_ASSERT( *get( &our_set, i ) == i ); + { + el = get( &our_set, i ); + ALWAYS_ASSERT( el && *el == i ); + } } cleanup( &our_set );