hashtable v0.1.1
A lightweight separate-chaining arena-backed hashtable in C
A lightweight separate-chaining, arena-backed hashtable in C

Introduction

See API documentation for hashtable_api.h .

This module implements a lightweight hashtable that uses separate chaining to resolve collisions.

This hashtable is designed to be flexible enough for use on embedded systems that have no dynamic memory, and/or limited memory in general.

Getting started

  1. Compile hashtable.c along with the rest of the files in your project
  2. Include hashtable_api.h in files where you want to use hashtables
  3. Use the functions detailed in hashtable_api.h to create and interact with hashtables

Example program

The following sample program creates a hashtable instance, inserts a single key/value pair, and the retrieves the stored value with the same key, and prints it to stdout.

See API documentation for hashtable_api.h for comprehensive details about all available functions.

#include <stdio.h>
#include <string.h>
#include "hashtable_api.h"
// Generates a new key/value pair on the stack and inserts into the given table
static int _insert_item(hashtable_t *table)
{
char key[32u];
char value[32u];
int keysize = snprintf(key, sizeof(key), "My key #1");
int valuesize = snprintf(value, sizeof(value), "My value #1");
return hashtable_insert(table, key, keysize + 1, value, valuesize + 1);
}
// Retrieves the value from the given table using the same key, and prints it
static int _retrieve_and_print_item(hashtable_t *table)
{
const char *key = "My key #1";
size_t keysize = strlen(key) + 1u;
char *value;
hashtable_size_t valuesize;
if (hashtable_retrieve(table, key, keysize, &value, &valuesize) != 0)
{
return -1;
}
printf("key='%s', value='%s', valuesize=%d\n", key, value, valuesize);
return 0;
}
int main(void)
{
// Hashtable instance
hashtable_t table;
// Allocate buffer of size 512 for storing the table + key/value data
char buf[512];
// Initialize a hashtable instance with 512 byte buffer
if (hashtable_create(&table, NULL, buf, sizeof(buf)) != 0)
{
// Error
return -1;
}
// Add a key/value pair to the hashtable
if (_insert_item(&table) != 0)
{
// Error
return -1;
}
// Retrieve inserted item and print it
return _retrieve_and_print_item(&table);
}
Implements a lightweight separate-chaining hashtable designed to be flexible enough for embedded syst...
int hashtable_create(hashtable_t *table, const hashtable_config_t *config, void *buffer, size_t buffer_size)
int hashtable_retrieve(hashtable_t *table, const char *key, const hashtable_size_t key_size, char **value, hashtable_size_t *value_size)
int hashtable_insert(hashtable_t *table, const char *key, const hashtable_size_t key_size, const char *value, const hashtable_size_t value_size)
size_t hashtable_size_t
Defines the type used to represent the size of keys and values stored in the hashtable.
Definition: hashtable_api.h:134
All data for a single hashtable instance.
Definition: hashtable_api.h:183

Performance visualization

The following graph shows the results from a test program which creates a hashtable instance with a 32MB buffer, and inserts items until the buffer is full (each key is a 32-bit unsigned integer, and all values are NULL / 0 bytes).

After every 2,000 items inserted, the test program performs the following checks;

  • Divide the time taken for the last 2,000 item insertions, by 2,000, to get the average insertion time
  • Retrieve all items in the table, and divide the time taken by the number of items in the table, to get the average item retrieval time
  • Generate 1,000 keys that are not in the table, and check if they exist using hashtable_has_key. Divide the time taken for checking all keys by 1000 to get the average time to check for a bad key.

Test was compiled with "-O2" optimization level and executed on a system with an Intel Core-i7 running Windows 10.

Features/limitations

  • Implemented in pure C99, and requires only stdint.h and string.h.
  • Uses separate chaining to resolve collisions.
  • Keys and values are byte streams of arbitrary length/contents, so keys and values can be any data type.
  • No dynamic memory allocation, and no table re-sizing. All table data is stored in a buffer that must be provided by the caller on hashtable creation, and when there is not enough space remaining in that buffer, insertion of new items will fail.
  • Provide/write your own hash function (FNV-1a is used by default if you don't provide one).

Build/compile options

There are a number of preprocessor symbols you can define to change certain things, here are the details about those symbols.

Datatype used for key/value sizes

By default, size_t is used to hold the sizes of keys/values. If you know that all your keys/values are below a certain size, however, and you want to save some memory, then you can define one of the following options to set the datatype used to hold key/value sizes:

Symbol name Effect
HASHTABLE_SIZE_T_UINT16 hashtable_size_t is uint16_t
HASHTABLE_SIZE_T_UINT32 hashtable_size_t is uint32_t
HASHTABLE_SIZE_T_SYS hashtable_size_t is size_t (default)

Disable function parameter validation

By default, all function parameters are checked for validity (e.g. no NULL pointers, no size values of 0). However, if you have a stable system and have worked most of those types of bugs out, then you may want to compile these checks out for a performance gain. Define the following option to compile out all function parameter validation checks:

Symbol name Effect
HASHTABLE_DISABLE_PARAM_VALIDATION All function param. validation checks compiled out

Enable packed struct option

Use __attribute__((packed)) for key/value pair struct definition, may save some extra space for stored key/value pair data:

Symbol name Effect
HASHTABLE_PACKED_STRUCT Key/value pair struct uses __attribute__((packed))