Quelle int-map.c
Sprache: C
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright 2023 Red Hat
*/
/**
* DOC :
*
* Hash table implementation of a map from integers to pointers , implemented using the Hopscotch
* Hashing algorithm by Herlihy , Shavit , and Tzafrir ( see
* http : //en.wikipedia.org/wiki/Hopscotch_hashing). This implementation does not contain any of the
* locking / concurrency features of the algorithm , just the collision resolution scheme .
*
* Hopscotch Hashing is based on hashing with open addressing and linear probing . All the entries
* are stored in a fixed array of buckets , with no dynamic allocation for collisions . Unlike linear
* probing , all the entries that hash to a given bucket are stored within a fixed neighborhood
* starting at that bucket . Chaining is effectively represented as a bit vector relative to each
* bucket instead of as pointers or explicit offsets .
*
* When an empty bucket cannot be found within a given neighborhood , subsequent neighborhoods are
* searched , and one or more entries will " hop " into those neighborhoods . When this process works ,
* an empty bucket will move into the desired neighborhood , allowing the entry to be added . When
* that process fails ( typically when the buckets are around 90 % full ) , the table must be resized
* and the all entries rehashed and added to the expanded table .
*
* Unlike linear probing , the number of buckets that must be searched in the worst case has a fixed
* upper bound ( the size of the neighborhood ) . Those entries occupy a small number of memory cache
* lines , leading to improved use of the cache ( fewer misses on both successful and unsuccessful
* searches ) . Hopscotch hashing outperforms linear probing at much higher load factors , so even
* with the increased memory burden for maintaining the hop vectors , less memory is needed to
* achieve that performance . Hopscotch is also immune to " contamination " from deleting entries
* since entries are genuinely removed instead of being replaced by a placeholder .
*
* The published description of the algorithm used a bit vector , but the paper alludes to an offset
* scheme which is used by this implementation . Since the entries in the neighborhood are within N
* entries of the hash bucket at the start of the neighborhood , a pair of small offset fields each
* log2 ( N ) bits wide is all that ' s needed to maintain the hops as a linked list . In order to encode
* " no next hop " ( i . e . NULL ) as the natural initial value of zero , the offsets are biased by one
* ( i . e . 0 = > NULL , 1 = > offset = 0 , 2 = > offset = 1 , etc . ) We can represent neighborhoods of up to 255
* entries with just 8 + 8 = 16 bits per entry . The hop list is sorted by hop offset so the first entry
* in the list is always the bucket closest to the start of the neighborhood .
*
* While individual accesses tend to be very fast , the table resize operations are very , very
* expensive . If an upper bound on the latency of adding an entry to the table is needed , we either
* need to ensure the table is pre - sized to be large enough so no resize is ever needed , or we ' ll
* need to develop an approach to incrementally resize the table .
*/
#include "int-map.h"
#include <linux/minmax.h>
#include "errors.h"
#include "logger.h"
#include "memory-alloc.h"
#include "numeric.h"
#include "permassert.h"
#define DEFAULT_CAPACITY 16 /* the number of neighborhoods in a new table */
#define NEIGHBORHOOD 255 /* the number of buckets in each neighborhood */
#define MAX_PROBES 1024 /* limit on the number of probes for a free bucket */
#define NULL_HOP_OFFSET 0 /* the hop offset value terminating the hop list */
#define DEFAULT_LOAD 75 /* a compromise between memory use and performance */
/**
* struct bucket - hash bucket
*
* Buckets are packed together to reduce memory usage and improve cache efficiency . It would be
* tempting to encode the hop offsets separately and maintain alignment of key / value pairs , but
* it ' s crucial to keep the hop fields near the buckets that they use them so they ' ll tend to share
* cache lines .
*/
struct bucket {
/**
* @ first_hop : The biased offset of the first entry in the hop list of the neighborhood
* that hashes to this bucket .
*/
u8 first_hop;
/** @next_hop: The biased offset of the next bucket in the hop list. */
u8 next_hop;
/** @key: The key stored in this bucket. */
u64 key;
/** @value: The value stored in this bucket (NULL if empty). */
void *value;
} __packed;
/**
* struct int_map - The concrete definition of the opaque int_map type .
*
* To avoid having to wrap the neighborhoods of the last entries back around to the start of the
* bucket array , we allocate a few more buckets at the end of the array instead , which is why
* capacity and bucket_count are different .
*/
struct int_map {
/** @size: The number of entries stored in the map. */
size_t size;
/** @capacity: The number of neighborhoods in the map. */
size_t capacity;
/** @bucket_count: The number of buckets in the bucket array. */
size_t bucket_count;
/** @buckets: The array of hash buckets. */
struct bucket *buckets;
};
/**
* mix ( ) - The Google CityHash 16 - byte hash mixing function .
* @ input1 : The first input value .
* @ input2 : The second input value .
*
* Return : A hash of the two inputs .
*/
static u64 mix(u64 input1, u64 input2)
{
static const u64 CITY_MULTIPLIER = 0 x9ddfea08eb382d69ULL;
u64 hash = (input1 ^ input2);
hash *= CITY_MULTIPLIER;
hash ^= (hash >> 47 );
hash ^= input2;
hash *= CITY_MULTIPLIER;
hash ^= (hash >> 47 );
hash *= CITY_MULTIPLIER;
return hash;
}
/**
* hash_key ( ) - Calculate a 64 - bit non - cryptographic hash value for the provided 64 - bit integer
* key .
* @ key : The mapping key .
*
* The implementation is based on Google ' s CityHash , only handling the specific case of an 8 - byte
* input .
*
* Return : The hash of the mapping key .
*/
static u64 hash_key(u64 key)
{
/*
* Aliasing restrictions forbid us from casting pointer types , so use a union to convert a
* single u64 to two u32 values .
*/
union {
u64 u64;
u32 u32[2 ];
} pun = {.u64 = key};
return mix(sizeof (key) + (((u64) pun.u32[0 ]) << 3 ), pun.u32[1 ]);
}
/**
* allocate_buckets ( ) - Initialize an int_map .
* @ map : The map to initialize .
* @ capacity : The initial capacity of the map .
*
* Return : VDO_SUCCESS or an error code .
*/
static int allocate_buckets(struct int_map *map, size_t capacity)
{
map->size = 0 ;
map->capacity = capacity;
/*
* Allocate NEIGHBORHOOD - 1 extra buckets so the last bucket can have a full neighborhood
* without have to wrap back around to element zero .
*/
map->bucket_count = capacity + (NEIGHBORHOOD - 1 );
return vdo_allocate(map->bucket_count, struct bucket,
"struct int_map buckets" , &map->buckets);
}
/**
* vdo_int_map_create ( ) - Allocate and initialize an int_map .
* @ initial_capacity : The number of entries the map should initially be capable of holding ( zero
* tells the map to use its own small default ) .
* @ map_ptr : Output , a pointer to hold the new int_map .
*
* Return : VDO_SUCCESS or an error code .
*/
int vdo_int_map_create(size_t initial_capacity, struct int_map **map_ptr)
{
struct int_map *map;
int result;
size_t capacity;
result = vdo_allocate(1 , struct int_map, "struct int_map" , &map);
if (result != VDO_SUCCESS)
return result;
/* Use the default capacity if the caller did not specify one. */
capacity = (initial_capacity > 0 ) ? initial_capacity : DEFAULT_CAPACITY;
/*
* Scale up the capacity by the specified initial load factor . ( i . e to hold 1000 entries at
* 80 % load we need a capacity of 1250 )
*/
capacity = capacity * 100 / DEFAULT_LOAD;
result = allocate_buckets(map, capacity);
if (result != VDO_SUCCESS) {
vdo_int_map_free(vdo_forget(map));
return result;
}
*map_ptr = map;
return VDO_SUCCESS;
}
/**
* vdo_int_map_free ( ) - Free an int_map .
* @ map : The int_map to free .
*
* NOTE : The map does not own the pointer values stored in the map and they are not freed by this
* call .
*/
void vdo_int_map_free(struct int_map *map)
{
if (map == NULL)
return ;
vdo_free(vdo_forget(map->buckets));
vdo_free(vdo_forget(map));
}
/**
* vdo_int_map_size ( ) - Get the number of entries stored in an int_map .
* @ map : The int_map to query .
*
* Return : The number of entries in the map .
*/
size_t vdo_int_map_size(const struct int_map *map)
{
return map->size;
}
/**
* dereference_hop ( ) - Convert a biased hop offset within a neighborhood to a pointer to the bucket
* it references .
* @ neighborhood : The first bucket in the neighborhood .
* @ hop_offset : The biased hop offset to the desired bucket .
*
* Return : NULL if hop_offset is zero , otherwise a pointer to the bucket in the neighborhood at
* hop_offset - 1 .
*/
static struct bucket *dereference_hop(struct bucket *neighborhood, unsigned int hop_offset)
{
BUILD_BUG_ON(NULL_HOP_OFFSET != 0 );
if (hop_offset == NULL_HOP_OFFSET)
return NULL;
return &neighborhood[hop_offset - 1 ];
}
/**
* insert_in_hop_list ( ) - Add a bucket into the hop list for the neighborhood .
* @ neighborhood : The first bucket in the neighborhood .
* @ new_bucket : The bucket to add to the hop list .
*
* The bucket is inserted it into the list so the hop list remains sorted by hop offset .
*/
static void insert_in_hop_list(struct bucket *neighborhood, struct bucket *new_bucket)
{
/* Zero indicates a NULL hop offset, so bias the hop offset by one. */
int hop_offset = 1 + (new_bucket - neighborhood);
/* Handle the special case of adding a bucket at the start of the list. */
int next_hop = neighborhood->first_hop;
if ((next_hop == NULL_HOP_OFFSET) || (next_hop > hop_offset)) {
new_bucket->next_hop = next_hop;
neighborhood->first_hop = hop_offset;
return ;
}
/* Search the hop list for the insertion point that maintains the sort order. */
for (;;) {
struct bucket *bucket = dereference_hop(neighborhood, next_hop);
next_hop = bucket->next_hop;
if ((next_hop == NULL_HOP_OFFSET) || (next_hop > hop_offset)) {
new_bucket->next_hop = next_hop;
bucket->next_hop = hop_offset;
return ;
}
}
}
/**
* select_bucket ( ) - Select and return the hash bucket for a given search key .
* @ map : The map to search .
* @ key : The mapping key .
*/
static struct bucket *select_bucket(const struct int_map *map, u64 key)
{
/*
* Calculate a good hash value for the provided key . We want exactly 32 bits , so mask the
* result .
*/
u64 hash = hash_key(key) & 0 xFFFFFFFF;
/*
* Scale the 32 - bit hash to a bucket index by treating it as a binary fraction and
* multiplying that by the capacity . If the hash is uniformly distributed over [ 0 . .
* 2 ^ 32 - 1 ] , then ( hash * capacity / 2 ^ 32 ) should be uniformly distributed over [ 0 . .
* capacity - 1 ] . The multiply and shift is much faster than a divide ( modulus ) on X86 CPUs .
*/
return &map->buckets[(hash * map->capacity) >> 32 ];
}
/**
* search_hop_list ( ) - Search the hop list associated with given hash bucket for a given search
* key .
* @ bucket : The map bucket to search for the key .
* @ key : The mapping key .
* @ previous_ptr : Output . if not NULL , a pointer in which to store the bucket in the list preceding
* the one that had the matching key
*
* If the key is found , returns a pointer to the entry ( bucket or collision ) , otherwise returns
* NULL .
*
* Return : An entry that matches the key , or NULL if not found .
*/
static struct bucket *search_hop_list(struct bucket *bucket, u64 key,
struct bucket **previous_ptr)
{
struct bucket *previous = NULL;
unsigned int next_hop = bucket->first_hop;
while (next_hop != NULL_HOP_OFFSET) {
/*
* Check the neighboring bucket indexed by the offset for the
* desired key .
*/
struct bucket *entry = dereference_hop(bucket, next_hop);
if ((key == entry->key) && (entry->value != NULL)) {
if (previous_ptr != NULL)
*previous_ptr = previous;
return entry;
}
next_hop = entry->next_hop;
previous = entry;
}
return NULL;
}
/**
* vdo_int_map_get ( ) - Retrieve the value associated with a given key from the int_map .
* @ map : The int_map to query .
* @ key : The key to look up .
*
* Return : The value associated with the given key , or NULL if the key is not mapped to any value .
*/
void *vdo_int_map_get(struct int_map *map, u64 key)
{
struct bucket *match = search_hop_list(select_bucket(map, key), key, NULL);
return ((match != NULL) ? match->value : NULL);
}
/**
* resize_buckets ( ) - Increase the number of hash buckets .
* @ map : The map to resize .
*
* Resizes and rehashes all the existing entries , storing them in the new buckets .
*
* Return : VDO_SUCCESS or an error code .
*/
static int resize_buckets(struct int_map *map)
{
int result;
size_t i;
/* Copy the top-level map data to the stack. */
struct int_map old_map = *map;
/* Re-initialize the map to be empty and 50% larger. */
size_t new_capacity = map->capacity / 2 * 3 ;
vdo_log_info("%s: attempting resize from %zu to %zu, current size=%zu" ,
__func__, map->capacity, new_capacity, map->size);
result = allocate_buckets(map, new_capacity);
if (result != VDO_SUCCESS) {
*map = old_map;
return result;
}
/* Populate the new hash table from the entries in the old bucket array. */
for (i = 0 ; i < old_map.bucket_count; i++) {
struct bucket *entry = &old_map.buckets[i];
if (entry->value == NULL)
continue ;
result = vdo_int_map_put(map, entry->key, entry->value, true , NULL);
if (result != VDO_SUCCESS) {
/* Destroy the new partial map and restore the map from the stack. */
vdo_free(vdo_forget(map->buckets));
*map = old_map;
return result;
}
}
/* Destroy the old bucket array. */
vdo_free(vdo_forget(old_map.buckets));
return VDO_SUCCESS;
}
/**
* find_empty_bucket ( ) - Probe the bucket array starting at the given bucket for the next empty
* bucket , returning a pointer to it .
* @ map : The map containing the buckets to search .
* @ bucket : The bucket at which to start probing .
* @ max_probes : The maximum number of buckets to search .
*
* NULL will be returned if the search reaches the end of the bucket array or if the number of
* linear probes exceeds a specified limit .
*
* Return : The next empty bucket , or NULL if the search failed .
*/
static struct bucket *
find_empty_bucket(struct int_map *map, struct bucket *bucket, unsigned int max_probes)
{
/*
* Limit the search to either the nearer of the end of the bucket array or a fixed distance
* beyond the initial bucket .
*/
ptrdiff_t remaining = &map->buckets[map->bucket_count] - bucket;
struct bucket *sentinel = &bucket[min_t(ptrdiff_t, remaining, max_probes)];
struct bucket *entry;
for (entry = bucket; entry < sentinel; entry++) {
if (entry->value == NULL)
return entry;
}
return NULL;
}
/**
* move_empty_bucket ( ) - Move an empty bucket closer to the start of the bucket array .
* @ hole : The empty bucket to fill with an entry that precedes it in one of its enclosing
* neighborhoods .
*
* This searches the neighborhoods that contain the empty bucket for a non - empty bucket closer to
* the start of the array . If such a bucket is found , this swaps the two buckets by moving the
* entry to the empty bucket .
*
* Return : The bucket that was vacated by moving its entry to the provided hole , or NULL if no
* entry could be moved .
*/
static struct bucket *move_empty_bucket(struct bucket *hole)
{
/*
* Examine every neighborhood that the empty bucket is part of , starting with the one in
* which it is the last bucket . No boundary check is needed for the negative array
* arithmetic since this function is only called when hole is at least NEIGHBORHOOD cells
* deeper into the array than a valid bucket .
*/
struct bucket *bucket;
for (bucket = &hole[1 - NEIGHBORHOOD]; bucket < hole; bucket++) {
/*
* Find the entry that is nearest to the bucket , which means it will be nearest to
* the hash bucket whose neighborhood is full .
*/
struct bucket *new_hole = dereference_hop(bucket, bucket->first_hop);
if (new_hole == NULL) {
/*
* There are no buckets in this neighborhood that are in use by this one
* ( they must all be owned by overlapping neighborhoods ) .
*/
continue ;
}
/*
* Skip this bucket if its first entry is actually further away than the hole that
* we ' re already trying to fill .
*/
if (hole < new_hole)
continue ;
/*
* We ' ve found an entry in this neighborhood that we can " hop " further away , moving
* the hole closer to the hash bucket , if not all the way into its neighborhood .
*/
/*
* The entry that will be the new hole is the first bucket in the list , so setting
* first_hop is all that ' s needed remove it from the list .
*/
bucket->first_hop = new_hole->next_hop;
new_hole->next_hop = NULL_HOP_OFFSET;
/* Move the entry into the original hole. */
hole->key = new_hole->key;
hole->value = new_hole->value;
new_hole->value = NULL;
/* Insert the filled hole into the hop list for the neighborhood. */
insert_in_hop_list(bucket, hole);
return new_hole;
}
/* We couldn't find an entry to relocate to the hole. */
return NULL;
}
/**
* update_mapping ( ) - Find and update any existing mapping for a given key , returning the value
* associated with the key in the provided pointer .
* @ neighborhood : The first bucket in the neighborhood that would contain the search key
* @ key : The key with which to associate the new value .
* @ new_value : The value to be associated with the key .
* @ update : Whether to overwrite an existing value .
* @ old_value_ptr : a pointer in which to store the old value ( unmodified if no mapping was found )
*
* Return : true if the map contains a mapping for the key , false if it does not .
*/
static bool update_mapping(struct bucket *neighborhood, u64 key, void *new_value,
bool update, void **old_value_ptr)
{
struct bucket *bucket = search_hop_list(neighborhood, key, NULL);
if (bucket == NULL) {
/* There is no bucket containing the key in the neighborhood. */
return false ;
}
/*
* Return the value of the current mapping ( if desired ) and update the mapping with the new
* value ( if desired ) .
*/
if (old_value_ptr != NULL)
*old_value_ptr = bucket->value;
if (update)
bucket->value = new_value;
return true ;
}
/**
* find_or_make_vacancy ( ) - Find an empty bucket .
* @ map : The int_map to search or modify .
* @ neighborhood : The first bucket in the neighborhood in which an empty bucket is needed for a new
* mapping .
*
* Find an empty bucket in a specified neighborhood for a new mapping or attempt to re - arrange
* mappings so there is such a bucket . This operation may fail ( returning NULL ) if an empty bucket
* is not available or could not be relocated to the neighborhood .
*
* Return : a pointer to an empty bucket in the desired neighborhood , or NULL if a vacancy could not
* be found or arranged .
*/
static struct bucket *find_or_make_vacancy(struct int_map *map,
struct bucket *neighborhood)
{
/* Probe within and beyond the neighborhood for the first empty bucket. */
struct bucket *hole = find_empty_bucket(map, neighborhood, MAX_PROBES);
/*
* Keep trying until the empty bucket is in the bucket ' s neighborhood or we are unable to
* move it any closer by swapping it with a filled bucket .
*/
while (hole != NULL) {
int distance = hole - neighborhood;
if (distance < NEIGHBORHOOD) {
/*
* We ' ve found or relocated an empty bucket close enough to the initial
* hash bucket to be referenced by its hop vector .
*/
return hole;
}
/*
* The nearest empty bucket isn ' t within the neighborhood that must contain the new
* entry , so try to swap it with bucket that is closer .
*/
hole = move_empty_bucket(hole);
}
return NULL;
}
/**
* vdo_int_map_put ( ) - Try to associate a value with an integer .
* @ map : The int_map to attempt to modify .
* @ key : The key with which to associate the new value .
* @ new_value : The value to be associated with the key .
* @ update : Whether to overwrite an existing value .
* @ old_value_ptr : A pointer in which to store either the old value ( if the key was already mapped )
* or NULL if the map did not contain the key ; NULL may be provided if the caller
* does not need to know the old value
*
* Try to associate a value ( a pointer ) with an integer in an int_map . If the map already contains
* a mapping for the provided key , the old value is only replaced with the specified value if
* update is true . In either case the old value is returned . If the map does not already contain a
* value for the specified key , the new value is added regardless of the value of update .
*
* Return : VDO_SUCCESS or an error code .
*/
int vdo_int_map_put(struct int_map *map, u64 key, void *new_value, bool update,
void **old_value_ptr)
{
struct bucket *neighborhood, *bucket;
if (unlikely(new_value == NULL))
return -EINVAL;
/*
* Select the bucket at the start of the neighborhood that must contain any entry for the
* provided key .
*/
neighborhood = select_bucket(map, key);
/*
* Check whether the neighborhood already contains an entry for the key , in which case we
* optionally update it , returning the old value .
*/
if (update_mapping(neighborhood, key, new_value, update, old_value_ptr))
return VDO_SUCCESS;
/*
* Find an empty bucket in the desired neighborhood for the new entry or re - arrange entries
* in the map so there is such a bucket . This operation will usually succeed ; the loop body
* will only be executed on the rare occasions that we have to resize the map .
*/
while ((bucket = find_or_make_vacancy(map, neighborhood)) == NULL) {
int result;
/*
* There is no empty bucket in which to put the new entry in the current map , so
* we ' re forced to allocate a new bucket array with a larger capacity , re - hash all
* the entries into those buckets , and try again ( a very expensive operation for
* large maps ) .
*/
result = resize_buckets(map);
if (result != VDO_SUCCESS)
return result;
/*
* Resizing the map invalidates all pointers to buckets , so recalculate the
* neighborhood pointer .
*/
neighborhood = select_bucket(map, key);
}
/* Put the new entry in the empty bucket, adding it to the neighborhood. */
bucket->key = key;
bucket->value = new_value;
insert_in_hop_list(neighborhood, bucket);
map->size += 1 ;
/* There was no existing entry, so there was no old value to be returned. */
if (old_value_ptr != NULL)
*old_value_ptr = NULL;
return VDO_SUCCESS;
}
/**
* vdo_int_map_remove ( ) - Remove the mapping for a given key from the int_map .
* @ map : The int_map from which to remove the mapping .
* @ key : The key whose mapping is to be removed .
*
* Return : the value that was associated with the key , or NULL if it was not mapped .
*/
void *vdo_int_map_remove(struct int_map *map, u64 key)
{
void *value;
/* Select the bucket to search and search it for an existing entry. */
struct bucket *bucket = select_bucket(map, key);
struct bucket *previous;
struct bucket *victim = search_hop_list(bucket, key, &previous);
if (victim == NULL) {
/* There is no matching entry to remove. */
return NULL;
}
/*
* We found an entry to remove . Save the mapped value to return later and empty the bucket .
*/
map->size -= 1 ;
value = victim->value;
victim->value = NULL;
victim->key = 0 ;
/* The victim bucket is now empty, but it still needs to be spliced out of the hop list. */
if (previous == NULL) {
/* The victim is the head of the list, so swing first_hop. */
bucket->first_hop = victim->next_hop;
} else {
previous->next_hop = victim->next_hop;
}
victim->next_hop = NULL_HOP_OFFSET;
return value;
}
Messung V0.5 in Prozent C=90 H=92 G=90
¤ Dauer der Verarbeitung: 0.19 Sekunden
(vorverarbeitet am 2026-09-28)
¤
*© Formatika GbR, Deutschland
2026-10-10
Neuigkeiten
Aktuelles
Motto des Tages
letze Version des Elbe Quellennavigators
Jenseits des Üblichen ....
Besucher
Statistik
#Sources=1126864
#Domains=1897691