This commit is contained in:
2026-02-26 13:05:16 +01:00
commit 87bbca2449
9 changed files with 1961 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.zig-cache
zig-out

11
LICENSE Normal file
View File

@@ -0,0 +1,11 @@
Copyright 2026 Pascal Zittlau
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

52
README.md Normal file
View File

@@ -0,0 +1,52 @@
# HashMapConcurrent
A thread-safe, fixed-capacity, open-addressing hash map for Zig.
This implementation combines *Robin Hood hashing* (to minimize probe lengths) with *Sequence
Locking* (to provide wait-free-like read performance) and *Shard-Level Locking* for writers.
Deletions use *Backward-Shift* to maintain table compactness without the performance degradation
of tombstones.
![Benchmark Results](benchmark_results.png)
## Quick Start
```zig
const std = @import("std");
const HashMap = @import("hashmap_concurrent.zig").AutoHashMapConcurrent;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
// capacity must be a power of two.
// num_shards balances writer contention vs reader retry probability.
var map = try HashMap(u64, u64).init(allocator, 1024, 64);
defer map.deinit(allocator);
map.put(42, 1337);
const val = map.get(42);
std.debug.print("Value: {d}\n", .{val});
}
```
## Iteration
There are two ways to iterate over entries, depending on your consistency requirements:
`lockingIterator()`: Uses *Lock Coupling* to prevent elements from being missed or seen twice if they
are moved across shard boundaries during iteration. Due to the locking you must call `it.deinit()`
if you break or return from the loop early to release the held shard lock.
`approximateIterator()`: Optimistic and approximate because it just uses *Sequence Locks*. It may
miss entries or see the same entry twice if concurrent writers move elements. It's Lock-free and
safe to use on const references. Safe to break early without cleanup.
## Usage & Safety
For a detailed explanation of the concurrency model, deadlock safety, and memory reclamation, please
refer to the documentation at the top of `hashmap_concurrent.zig`.
## License
BSD 3-Clause. See `hashmap_concurrent.zig` and `LICENSE` for the full text.

73
benchmark_results.csv Normal file
View File

@@ -0,0 +1,73 @@
implementation,load_factor,workload,threads,time_ns,ops_per_sec
Concurrent,0.50,Read-Heavy,1,271214274,36871215
Concurrent,0.50,Read-Heavy,2,143124738,69869123
Concurrent,0.50,Read-Heavy,4,87717673,114002112
Concurrent,0.50,Read-Heavy,8,52661958,189890394
Concurrent,0.50,Balanced,1,329395173,30358671
Concurrent,0.50,Balanced,2,185577943,53885714
Concurrent,0.50,Balanced,4,125742054,79527888
Concurrent,0.50,Balanced,8,97082480,103005197
Concurrent,0.50,Write-Heavy,1,338436603,29547631
Concurrent,0.50,Write-Heavy,2,210645228,47473185
Concurrent,0.50,Write-Heavy,4,161165800,62047903
Concurrent,0.50,Write-Heavy,8,147405663,67839998
Concurrent,0.80,Read-Heavy,1,350774337,28508356
Concurrent,0.80,Read-Heavy,2,176895999,56530391
Concurrent,0.80,Read-Heavy,4,112098347,89207381
Concurrent,0.80,Read-Heavy,8,66763988,149781346
Mutex,0.80,Read-Heavy,1,118618369,84303974
Mutex,0.80,Read-Heavy,2,232759824,42962740
Mutex,0.80,Read-Heavy,4,387506802,25805998
Mutex,0.80,Read-Heavy,8,482177910,20739232
Concurrent,0.80,Balanced,1,446312104,22405845
Concurrent,0.80,Balanced,2,241178563,41463054
Concurrent,0.80,Balanced,4,159215445,62807976
Concurrent,0.80,Balanced,8,114053320,87678289
Mutex,0.80,Balanced,1,395063061,25312414
Mutex,0.80,Balanced,2,712581877,14033475
Mutex,0.80,Balanced,4,1031445991,9695127
Mutex,0.80,Balanced,8,1581436960,6323363
Concurrent,0.80,Write-Heavy,1,513414397,19477443
Concurrent,0.80,Write-Heavy,2,291053875,34357900
Concurrent,0.80,Write-Heavy,4,204070623,49002643
Concurrent,0.80,Write-Heavy,8,158653499,63030440
Mutex,0.80,Write-Heavy,1,660978068,15129095
Mutex,0.80,Write-Heavy,2,1179866451,8475535
Mutex,0.80,Write-Heavy,4,1704761281,5865923
Mutex,0.80,Write-Heavy,8,2417159373,4137087
Concurrent,0.90,Read-Heavy,1,417094992,23975353
Concurrent,0.90,Read-Heavy,2,211592438,47260668
Concurrent,0.90,Read-Heavy,4,130765194,76472948
Concurrent,0.90,Read-Heavy,8,83629750,119574672
Concurrent,0.90,Balanced,1,685793218,14581654
Concurrent,0.90,Balanced,2,367383739,27219495
Concurrent,0.90,Balanced,4,242404207,41253409
Concurrent,0.90,Balanced,8,166020753,60233433
Concurrent,0.90,Write-Heavy,1,914211584,10938386
Concurrent,0.90,Write-Heavy,2,491254857,20356032
Concurrent,0.90,Write-Heavy,4,330013876,30301756
Concurrent,0.90,Write-Heavy,8,236983500,42197030
Concurrent,0.95,Read-Heavy,1,546474852,18299103
Concurrent,0.95,Read-Heavy,2,288135632,34705877
Concurrent,0.95,Read-Heavy,4,185975581,53770500
Concurrent,0.95,Read-Heavy,8,127102271,78676800
Concurrent,0.95,Balanced,1,1510859227,6618750
Concurrent,0.95,Balanced,2,796566144,12553885
Concurrent,0.95,Balanced,4,545869033,18319412
Concurrent,0.95,Balanced,8,362585843,27579675
Concurrent,0.95,Write-Heavy,1,2399725873,4167142
Concurrent,0.95,Write-Heavy,2,1264147766,7910467
Concurrent,0.95,Write-Heavy,4,870680794,11485265
Concurrent,0.95,Write-Heavy,8,610506395,16379844
Concurrent,0.98,Read-Heavy,1,1185684905,8433943
Concurrent,0.98,Read-Heavy,2,747856212,13371554
Concurrent,0.98,Read-Heavy,4,557728969,17929855
Concurrent,0.98,Read-Heavy,8,412282979,24255185
Concurrent,0.98,Balanced,1,6334692279,1578608
Concurrent,0.98,Balanced,2,3499583498,2857482
Concurrent,0.98,Balanced,4,2676704901,3735936
Concurrent,0.98,Balanced,8,1723361504,5802613
Concurrent,0.98,Write-Heavy,1,13658053465,732168
Concurrent,0.98,Write-Heavy,2,7398089149,1351700
Concurrent,0.98,Write-Heavy,4,5440264030,1838146
Concurrent,0.98,Write-Heavy,8,3359251517,2976853
1 implementation load_factor workload threads time_ns ops_per_sec
2 Concurrent 0.50 Read-Heavy 1 271214274 36871215
3 Concurrent 0.50 Read-Heavy 2 143124738 69869123
4 Concurrent 0.50 Read-Heavy 4 87717673 114002112
5 Concurrent 0.50 Read-Heavy 8 52661958 189890394
6 Concurrent 0.50 Balanced 1 329395173 30358671
7 Concurrent 0.50 Balanced 2 185577943 53885714
8 Concurrent 0.50 Balanced 4 125742054 79527888
9 Concurrent 0.50 Balanced 8 97082480 103005197
10 Concurrent 0.50 Write-Heavy 1 338436603 29547631
11 Concurrent 0.50 Write-Heavy 2 210645228 47473185
12 Concurrent 0.50 Write-Heavy 4 161165800 62047903
13 Concurrent 0.50 Write-Heavy 8 147405663 67839998
14 Concurrent 0.80 Read-Heavy 1 350774337 28508356
15 Concurrent 0.80 Read-Heavy 2 176895999 56530391
16 Concurrent 0.80 Read-Heavy 4 112098347 89207381
17 Concurrent 0.80 Read-Heavy 8 66763988 149781346
18 Mutex 0.80 Read-Heavy 1 118618369 84303974
19 Mutex 0.80 Read-Heavy 2 232759824 42962740
20 Mutex 0.80 Read-Heavy 4 387506802 25805998
21 Mutex 0.80 Read-Heavy 8 482177910 20739232
22 Concurrent 0.80 Balanced 1 446312104 22405845
23 Concurrent 0.80 Balanced 2 241178563 41463054
24 Concurrent 0.80 Balanced 4 159215445 62807976
25 Concurrent 0.80 Balanced 8 114053320 87678289
26 Mutex 0.80 Balanced 1 395063061 25312414
27 Mutex 0.80 Balanced 2 712581877 14033475
28 Mutex 0.80 Balanced 4 1031445991 9695127
29 Mutex 0.80 Balanced 8 1581436960 6323363
30 Concurrent 0.80 Write-Heavy 1 513414397 19477443
31 Concurrent 0.80 Write-Heavy 2 291053875 34357900
32 Concurrent 0.80 Write-Heavy 4 204070623 49002643
33 Concurrent 0.80 Write-Heavy 8 158653499 63030440
34 Mutex 0.80 Write-Heavy 1 660978068 15129095
35 Mutex 0.80 Write-Heavy 2 1179866451 8475535
36 Mutex 0.80 Write-Heavy 4 1704761281 5865923
37 Mutex 0.80 Write-Heavy 8 2417159373 4137087
38 Concurrent 0.90 Read-Heavy 1 417094992 23975353
39 Concurrent 0.90 Read-Heavy 2 211592438 47260668
40 Concurrent 0.90 Read-Heavy 4 130765194 76472948
41 Concurrent 0.90 Read-Heavy 8 83629750 119574672
42 Concurrent 0.90 Balanced 1 685793218 14581654
43 Concurrent 0.90 Balanced 2 367383739 27219495
44 Concurrent 0.90 Balanced 4 242404207 41253409
45 Concurrent 0.90 Balanced 8 166020753 60233433
46 Concurrent 0.90 Write-Heavy 1 914211584 10938386
47 Concurrent 0.90 Write-Heavy 2 491254857 20356032
48 Concurrent 0.90 Write-Heavy 4 330013876 30301756
49 Concurrent 0.90 Write-Heavy 8 236983500 42197030
50 Concurrent 0.95 Read-Heavy 1 546474852 18299103
51 Concurrent 0.95 Read-Heavy 2 288135632 34705877
52 Concurrent 0.95 Read-Heavy 4 185975581 53770500
53 Concurrent 0.95 Read-Heavy 8 127102271 78676800
54 Concurrent 0.95 Balanced 1 1510859227 6618750
55 Concurrent 0.95 Balanced 2 796566144 12553885
56 Concurrent 0.95 Balanced 4 545869033 18319412
57 Concurrent 0.95 Balanced 8 362585843 27579675
58 Concurrent 0.95 Write-Heavy 1 2399725873 4167142
59 Concurrent 0.95 Write-Heavy 2 1264147766 7910467
60 Concurrent 0.95 Write-Heavy 4 870680794 11485265
61 Concurrent 0.95 Write-Heavy 8 610506395 16379844
62 Concurrent 0.98 Read-Heavy 1 1185684905 8433943
63 Concurrent 0.98 Read-Heavy 2 747856212 13371554
64 Concurrent 0.98 Read-Heavy 4 557728969 17929855
65 Concurrent 0.98 Read-Heavy 8 412282979 24255185
66 Concurrent 0.98 Balanced 1 6334692279 1578608
67 Concurrent 0.98 Balanced 2 3499583498 2857482
68 Concurrent 0.98 Balanced 4 2676704901 3735936
69 Concurrent 0.98 Balanced 8 1723361504 5802613
70 Concurrent 0.98 Write-Heavy 1 13658053465 732168
71 Concurrent 0.98 Write-Heavy 2 7398089149 1351700
72 Concurrent 0.98 Write-Heavy 4 5440264030 1838146
73 Concurrent 0.98 Write-Heavy 8 3359251517 2976853

BIN
benchmark_results.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

36
build.zig Normal file
View File

@@ -0,0 +1,36 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("hashmap_concurrent", .{
.root_source_file = b.path("hashmap_concurrent.zig"),
.target = target,
});
const exe = b.addExecutable(.{
.name = "benchmark",
.root_module = b.createModule(.{
.root_source_file = b.path("hashmap_concurrent.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_step = b.step("bench", "Run the benchmark");
const run_cmd = b.addRunArtifact(exe);
run_step.dependOn(&run_cmd.step);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}

13
build.zig.zon Normal file
View File

@@ -0,0 +1,13 @@
.{
.name = .hashmap_concurrent,
// This is a [Semantic Version](https://semver.org/).
.version = "0.1.0",
.fingerprint = 0x2614796ee2b381e, // Changing this has security and trust implications.
.minimum_zig_version = "0.15.2",
.paths = .{
"build.zig",
"build.zig.zon",
"hashmap_concurrent.zig",
"LICENSE",
},
}

1698
hashmap_concurrent.zig Normal file

File diff suppressed because it is too large Load Diff

76
plot.gp Normal file
View File

@@ -0,0 +1,76 @@
# Output Settings
set terminal pngcairo size 1200,1600 enhanced font 'Segoe UI,12' linewidth 2
set output 'benchmark_results.png'
# Data Settings
set datafile separator ","
set title font "Segoe UI,16"
# Layout: 3 rows, 1 column
set multiplot layout 3,1 title "Concurrent HashMap Benchmark Results\nThroughput vs Thread Count" offset 0, -0.05 scale 1, 0.95
# Axis Settings
set grid y
set grid x
set xlabel "Threads (Log Scale)"
set ylabel "Throughput (Operations / Sec)"
set logscale x 2
set format y "%.1s%c" # Formats 1000000 as 1.0M
# Key (Legend) Settings
set key outside right top box
set key title "Configuration"
# Colors (Semantic Spectrum: Cool to Critical)
c_050 = "#17becf" # Cyan (Light Load)
c_080 = "#2ca02c" # Green (Standard)
c_090 = "#1f77b4" # Blue (Heavy)
c_095 = "#ff7f0e" # Orange (Warning)
c_098 = "#d62728" # Red (Critical)
# Point Types (Shapes)
# 13=Diamond, 7=Circle, 5=Square, 9=Triangle Up, 11=Triangle Down
pt_conc_050 = 13
pt_conc_080 = 7
pt_conc_090 = 5
pt_conc_095 = 9
pt_conc_098 = 11
# Open versions for Mutex (Index - 1 usually)
pt_mutex_050 = 12
pt_mutex_080 = 6
pt_mutex_090 = 4
pt_mutex_095 = 8
pt_mutex_098 = 10
# Helper function to filter data
# Col 1: Impl, Col 2: LF, Col 3: Workload, Col 6: Ops/Sec
filter(workload, impl, lf) = (strcol(3) eq workload && strcol(1) eq impl && abs($2 - lf) < 0.001) ? $6 : 1/0
set title "Workload: Read-Heavy (3% Put, 2% Remove, 95% Get)"
plot \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Concurrent", 0.50)) w lp lc rgb c_050 pt pt_conc_050 t "Concurrent (LF 0.50)", \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Concurrent", 0.80)) w lp lc rgb c_080 pt pt_conc_080 t "Concurrent (LF 0.80)", \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Concurrent", 0.90)) w lp lc rgb c_090 pt pt_conc_090 t "Concurrent (LF 0.90)", \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Concurrent", 0.95)) w lp lc rgb c_095 pt pt_conc_095 t "Concurrent (LF 0.95)", \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Concurrent", 0.98)) w lp lc rgb c_098 pt pt_conc_098 t "Concurrent (LF 0.98)", \
'benchmark_results.csv' every ::1 using 4:(filter("Read-Heavy", "Mutex", 0.80)) w lp lc rgb c_080 dt 2 pt pt_mutex_080 t "Mutex (LF 0.80)",
set title "Workload: Balanced (25% Put, 25% Remove, 50% Get)"
plot \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Concurrent", 0.50)) w lp lc rgb c_050 pt pt_conc_050 t "Concurrent (LF 0.50)", \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Concurrent", 0.80)) w lp lc rgb c_080 pt pt_conc_080 t "Concurrent (LF 0.80)", \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Concurrent", 0.90)) w lp lc rgb c_090 pt pt_conc_090 t "Concurrent (LF 0.90)", \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Concurrent", 0.95)) w lp lc rgb c_095 pt pt_conc_095 t "Concurrent (LF 0.95)", \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Concurrent", 0.98)) w lp lc rgb c_098 pt pt_conc_098 t "Concurrent (LF 0.98)", \
'benchmark_results.csv' every ::1 using 4:(filter("Balanced", "Mutex", 0.80)) w lp lc rgb c_080 dt 2 pt pt_mutex_080 t "Mutex (LF 0.80)",
set title "Workload: Write-Heavy (45% Put, 45% Remove, 10% Get)"
plot \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Concurrent", 0.50)) w lp lc rgb c_050 pt pt_conc_050 t "Concurrent (LF 0.50)", \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Concurrent", 0.80)) w lp lc rgb c_080 pt pt_conc_080 t "Concurrent (LF 0.80)", \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Concurrent", 0.90)) w lp lc rgb c_090 pt pt_conc_090 t "Concurrent (LF 0.90)", \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Concurrent", 0.95)) w lp lc rgb c_095 pt pt_conc_095 t "Concurrent (LF 0.95)", \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Concurrent", 0.98)) w lp lc rgb c_098 pt pt_conc_098 t "Concurrent (LF 0.98)", \
'benchmark_results.csv' every ::1 using 4:(filter("Write-Heavy", "Mutex", 0.80)) w lp lc rgb c_080 dt 2 pt pt_mutex_080 t "Mutex (LF 0.80)",
unset multiplot