swiss_hash 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +166 -83
- data/ext/swiss_hash/swiss_hash.c +1164 -281
- data/lib/swiss_hash/version.rb +1 -1
- data/lib/swiss_hash.rb +37 -9
- metadata +4 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9d4f872535c0a7e7558f6c7950a2fb113263ab9b75cd2397bff72ee4185c295
|
|
4
|
+
data.tar.gz: ad110fb4e54d662181d3e49546eec833a953be7b87debae477837ad8e50408a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85cecddcb36ff970d62bd053bb78c7b42f0f15290276f64326ecc053236ca6ff473ed016933dd848ce9d9f4e5b63efdebb014140a84b22436e9bb9614567b4cf
|
|
7
|
+
data.tar.gz: 2cf36fed40554508684932d155bad203b4da18d9b808d2a8317b5f0e46b5a2e3dee9ebe3ab963ecdbd75406e99075d816d2dcf9b1277b562c6b421a845b1d8ec
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- NEON group matching on ARM64 (SSE2 on x86_64, SWAR elsewhere)
|
|
6
|
+
- Single allocation for control bytes and slots
|
|
7
|
+
- Faster Fixnum `[]` / `[]=`
|
|
8
|
+
- 7-bit strings of different encodings hash like Ruby Hash (`"abc"` and `"abc".b` are the same key)
|
|
9
|
+
- Rehash is GC-safe; `#hash` / `#eql?` that mutate the same table raise instead of corrupting it
|
|
10
|
+
- `initialize` no longer leaks on reuse; OOM no longer leaves a half-built table
|
|
11
|
+
|
|
12
|
+
## 0.1.2 - Unreleased
|
|
13
|
+
|
|
14
|
+
Documentation and API polish for the first public release candidate.
|
|
15
|
+
|
|
16
|
+
### Added / changed
|
|
17
|
+
|
|
18
|
+
- Expanded the Hash-like API surface:
|
|
19
|
+
- `fetch`, `values_at`, `fetch_values`
|
|
20
|
+
- `merge`, `merge!`, `update`, `replace`
|
|
21
|
+
- `to_h`, `to_sh`, `to_a`
|
|
22
|
+
- `slice`, `except`, `invert`, `assoc`, `rassoc`, `shift`
|
|
23
|
+
- `delete_if`, `keep_if`, `select`, `select!`, `filter`, `filter!`, `reject`, `reject!`
|
|
24
|
+
- `compact`, `compact!`, `transform_keys`, `transform_keys!`, `transform_values`, `transform_values!`
|
|
25
|
+
- `value?`, `has_value?`, `key`, `dig`, `count`, `flatten`, `==`, `eql?`, `hash`, `inspect`
|
|
26
|
+
- Moved the important Hash-like operations into the C extension instead of doing the expensive parts in Ruby.
|
|
27
|
+
- Kept `to_h` as the Ruby-compatible conversion to a real `Hash`; added `to_sh` as the explicit shallow SwissHash copy.
|
|
28
|
+
- Updated the README with the latest benchmark output from Ruby 3.4.3 / arm64-darwin24.
|
|
29
|
+
- Documented compatibility boundaries: no default values/default blocks, no `compare_by_identity`, no full insertion-order guarantee, and not the entire Ruby `Hash` API yet.
|
|
30
|
+
|
|
31
|
+
### Benchmark update
|
|
32
|
+
|
|
33
|
+
Struck-through values are the previous README numbers. The value after the arrow is the current benchmark result.
|
|
34
|
+
|
|
35
|
+
#### N = 100,000
|
|
36
|
+
|
|
37
|
+
| Operation | Ruby Hash | SwissHash | Delta |
|
|
38
|
+
|---|---:|---:|---:|
|
|
39
|
+
| Insert (string keys) | ~~17.6 ms~~ → 16.485 ms | ~~11.3 ms~~ → 10.386 ms | ~~−35.8%~~ → **−37.00%** |
|
|
40
|
+
| Delete + reinsert 25% | ~~9.8 ms~~ → 8.965 ms | ~~8.9 ms~~ → 7.164 ms | ~~−9.0%~~ → **−20.09%** |
|
|
41
|
+
| Insert (sequential int) | ~~7.0 ms~~ → 6.324 ms | ~~6.4 ms~~ → 5.023 ms | ~~−8.7%~~ → **−20.57%** |
|
|
42
|
+
| Mixed (70/20/10 R/W/D) | ~~21.4 ms~~ → 21.784 ms | ~~19.6 ms~~ → 18.990 ms | ~~−8.6%~~ → **−12.83%** |
|
|
43
|
+
| Insert (random int) | ~~6.7 ms~~ → 5.963 ms | ~~6.4 ms~~ → 5.010 ms | ~~−3.5%~~ → **−15.98%** |
|
|
44
|
+
| Lookup (string keys, 3x) | ~~20.3 ms~~ → 20.561 ms | ~~20.8 ms~~ → 21.535 ms | ~~+2.5%~~ → +4.74% |
|
|
45
|
+
| Lookup (sequential int, 3x) | ~~11.7 ms~~ → 13.281 ms | ~~12.2 ms~~ → 11.116 ms | ~~+4.4%~~ → **−16.31%** |
|
|
46
|
+
|
|
47
|
+
#### N = 10,000
|
|
48
|
+
|
|
49
|
+
| Operation | Ruby Hash | SwissHash | Delta |
|
|
50
|
+
|---|---:|---:|---:|
|
|
51
|
+
| Insert (string keys) | ~~1.63 ms~~ → 1.522 ms | ~~1.10 ms~~ → 0.992 ms | ~~−32.8%~~ → **−34.80%** |
|
|
52
|
+
| Lookup (string keys, 3x) | ~~1.71 ms~~ → 1.710 ms | ~~1.62 ms~~ → 1.563 ms | ~~−5.2%~~ → **−8.58%** |
|
|
53
|
+
| Mixed (70/20/10 R/W/D) | ~~1.93 ms~~ → 1.988 ms | ~~1.90 ms~~ → 1.869 ms | ~~−1.6%~~ → **−5.98%** |
|
|
54
|
+
| Delete + reinsert 25% | ~~0.91 ms~~ → 0.814 ms | ~~0.89 ms~~ → 0.714 ms | ~~−2.0%~~ → **−12.29%** |
|
|
55
|
+
| Insert (sequential int) | ~~0.66 ms~~ → 0.578 ms | ~~0.67 ms~~ → 0.510 ms | ~~+2.5%~~ → **−11.82%** |
|
|
56
|
+
| Lookup (sequential int, 3x) | ~~1.05 ms~~ → 1.071 ms | ~~1.12 ms~~ → 1.065 ms | ~~+6.0%~~ → −0.55% |
|
|
57
|
+
| Insert (random int) | previously not listed → 0.554 ms | previously not listed → 0.501 ms | **−9.53%** |
|
|
58
|
+
|
|
59
|
+
#### N = 1,000
|
|
60
|
+
|
|
61
|
+
| Operation | Ruby Hash | SwissHash | Delta |
|
|
62
|
+
|---|---:|---:|---:|
|
|
63
|
+
| Insert (string keys) | ~~0.183 ms~~ → 0.156 ms | ~~0.118 ms~~ → 0.102 ms | ~~−35.6%~~ → **−34.78%** |
|
|
64
|
+
| Lookup (string keys, 3x) | ~~0.184 ms~~ → 0.181 ms | ~~0.155 ms~~ → 0.150 ms | ~~−15.5%~~ → **−17.10%** |
|
|
65
|
+
| Insert (sequential int) | ~~0.063 ms~~ → 0.056 ms | ~~0.074 ms~~ → 0.057 ms | ~~+17.5%~~ → +1.88% |
|
|
66
|
+
| Delete + reinsert 25% | ~~0.094 ms~~ → 0.082 ms | ~~0.102 ms~~ → 0.079 ms | ~~+8.2%~~ → −4.12% |
|
|
67
|
+
| Insert (random int) | previously not listed → 0.054 ms | previously not listed → 0.051 ms | −6.54% |
|
|
68
|
+
| Lookup (sequential int, 3x) | previously not listed → 0.111 ms | previously not listed → 0.111 ms | +0.29% |
|
|
69
|
+
| Mixed (70/20/10 R/W/D) | previously not listed → 0.202 ms | previously not listed → 0.197 ms | −2.04% |
|
|
70
|
+
|
|
71
|
+
### Memory update
|
|
72
|
+
|
|
73
|
+
| Metric | Previous README | Current benchmark |
|
|
74
|
+
|---|---:|---:|
|
|
75
|
+
| SwissHash native memory, N=100,000 | ~~2,176 KB + 4 GC slots~~ | 2,176 KB native + 4 GC slots |
|
|
76
|
+
| Ruby Hash | ~~managed via GC slots / not directly measurable~~ | 3 GC slots; native memory not directly measurable |
|
|
77
|
+
| Load factor | ~~76.3%~~ | 76.3% |
|
|
78
|
+
| SIMD path | not shown in table | SWAR |
|
|
79
|
+
|
|
80
|
+
## 0.1.1 and earlier
|
|
81
|
+
|
|
82
|
+
Pre-release iterations focused on the initial Swiss Table C extension, Ruby object/GC integration, string-key fast paths, deletion/tombstone handling, and the first benchmark harness.
|
data/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# SwissHash
|
|
2
2
|
|
|
3
|
-
Swiss Table hash map implementation as a Ruby C extension.
|
|
3
|
+
Swiss Table hash map implementation as a Ruby C extension. The design follows the same broad family as Google's [Abseil](https://abseil.io/about/design/swisstables) `flat_hash_map`, Rust's [hashbrown](https://github.com/rust-lang/hashbrown), and [Go 1.24 Swiss Tables](https://go.dev/blog/swisstable), with Ruby-specific hashing, key preparation, GC integration, and a Hash-like API surface.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
```
|
|
7
|
+
```bash
|
|
8
8
|
gem install swiss_hash
|
|
9
9
|
```
|
|
10
10
|
|
|
@@ -15,72 +15,86 @@ require "swiss_hash"
|
|
|
15
15
|
|
|
16
16
|
h = SwissHash::Hash.new
|
|
17
17
|
h["key"] = "value"
|
|
18
|
-
h["key"]
|
|
19
|
-
h.
|
|
20
|
-
h.
|
|
18
|
+
h["key"] # => "value"
|
|
19
|
+
h.fetch("key") # => "value"
|
|
20
|
+
h.delete("key") # => "value"
|
|
21
|
+
h.stats # => { capacity: 16, size: 0, ... }
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
`SwissHash::Hash` is intentionally not a subclass of Ruby's built-in `Hash`. Use `to_h` when you need a real Ruby `Hash`, and `to_sh` when you want a shallow SwissHash copy.
|
|
25
|
+
|
|
23
26
|
## Performance Results
|
|
24
27
|
|
|
25
|
-
Benchmarks on Ruby 3.
|
|
28
|
+
Benchmarks below were produced by `benchmark.rb` on Ruby 3.4.3 / arm64-darwin24.
|
|
26
29
|
|
|
27
|
-
Methodology:
|
|
30
|
+
Methodology: 6 runs × 17 measured iterations, 4 warmup iterations per run, IQR-filtered mean per run, interleaved Ruby/SwissHash measurements with alternating start order, and per-side coefficient of variation (`±X.X%`) reported to make noise visible.
|
|
28
31
|
|
|
29
32
|
### N = 100,000
|
|
30
33
|
|
|
31
34
|
| Operation | Ruby Hash | SwissHash | Delta |
|
|
32
|
-
|
|
33
|
-
| Insert (
|
|
34
|
-
|
|
|
35
|
-
| Insert (
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
35
|
+
|---|---:|---:|---:|
|
|
36
|
+
| Insert (sequential int) | 7.004 ms (±2.4%) | 5.245 ms (±1.2%) | **−25.12%** ⚡ |
|
|
37
|
+
| Insert (string keys) | 17.751 ms (±1.0%) | 11.207 ms (±1.1%) | **−36.86%** ⚡ |
|
|
38
|
+
| Insert (random int) | 6.148 ms (±1.8%) | 4.845 ms (±0.2%) | **−21.20%** ⚡ |
|
|
39
|
+
| Lookup (sequential int, 3x) | 12.286 ms (±1.1%) | 11.614 ms (±1.1%) | **−5.47%** ⚡ |
|
|
40
|
+
| Lookup (string keys, 3x) | 22.558 ms (±0.6%) | 22.650 ms (±0.9%) | +0.41% |
|
|
41
|
+
| Delete + reinsert 25% | 9.517 ms (±0.8%) | 7.392 ms (±1.8%) | **−22.33%** ⚡ |
|
|
42
|
+
| Mixed (70% read / 20% write / 10% delete) | 25.333 ms (±2.0%) | 21.860 ms (±3.4%) | **−13.71%** ⚡ |
|
|
40
43
|
|
|
41
44
|
### N = 10,000
|
|
42
45
|
|
|
43
46
|
| Operation | Ruby Hash | SwissHash | Delta |
|
|
44
|
-
|
|
45
|
-
| Insert (
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
47
|
+
|---|---:|---:|---:|
|
|
48
|
+
| Insert (sequential int) | 0.606 ms (±2.8%) | 0.518 ms (±1.3%) | **−14.57%** ⚡ |
|
|
49
|
+
| Insert (string keys) | 1.638 ms (±1.9%) | 1.057 ms (±1.2%) | **−35.49%** ⚡ |
|
|
50
|
+
| Insert (random int) | 0.583 ms (±1.5%) | 0.507 ms (±1.2%) | **−13.07%** ⚡ |
|
|
51
|
+
| Lookup (sequential int, 3x) | 1.108 ms (±1.3%) | 1.063 ms (±1.4%) | **−4.02%** ⚡ |
|
|
52
|
+
| Lookup (string keys, 3x) | 1.748 ms (±1.5%) | 1.623 ms (±1.3%) | **−7.12%** ⚡ |
|
|
53
|
+
| Delete + reinsert 25% | 0.859 ms (±2.2%) | 0.740 ms (±2.2%) | **−13.85%** ⚡ |
|
|
54
|
+
| Mixed (70% read / 20% write / 10% delete) | 2.123 ms (±1.1%) | 1.928 ms (±1.2%) | **−9.21%** ⚡ |
|
|
51
55
|
|
|
52
56
|
### N = 1,000
|
|
53
57
|
|
|
54
|
-
Ruby Hash uses an AR-table
|
|
58
|
+
Ruby Hash uses an AR-table for small hashes, so very small integer-keyed workloads can still be close. String-heavy workloads remain the strongest SwissHash case.
|
|
55
59
|
|
|
56
60
|
| Operation | Ruby Hash | SwissHash | Delta |
|
|
57
|
-
|
|
58
|
-
| Insert (
|
|
59
|
-
|
|
|
60
|
-
| Insert (
|
|
61
|
-
|
|
|
61
|
+
|---|---:|---:|---:|
|
|
62
|
+
| Insert (sequential int) | 0.059 ms (±3.2%) | 0.057 ms (±4.6%) | **−4.32%** ⚡ |
|
|
63
|
+
| Insert (string keys) | 0.167 ms (±3.3%) | 0.108 ms (±1.8%) | **−35.17%** ⚡ |
|
|
64
|
+
| Insert (random int) | 0.055 ms (±2.1%) | 0.052 ms (±2.2%) | **−6.03%** ⚡ |
|
|
65
|
+
| Lookup (sequential int, 3x) | 0.114 ms (±1.8%) | 0.111 ms (±2.3%) | **−2.15%** ⚡ |
|
|
66
|
+
| Lookup (string keys, 3x) | 0.184 ms (±1.3%) | 0.155 ms (±0.9%) | **−15.68%** ⚡ |
|
|
67
|
+
| Delete + reinsert 25% | 0.086 ms (±2.2%) | 0.080 ms (±2.5%) | **−6.91%** ⚡ |
|
|
68
|
+
| Mixed (70% read / 20% write / 10% delete) | 0.211 ms (±1.4%) | 0.202 ms (±2.0%) | **−4.66%** ⚡ |
|
|
62
69
|
|
|
63
70
|
### Summary
|
|
64
71
|
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
72
|
+
- SwissHash is faster on **6 of 7 operations** at N=100k; string lookup at that size is within 1% of Ruby Hash.
|
|
73
|
+
- The strongest win is still string-key insertion: **−35% to −37%** across tested sizes.
|
|
74
|
+
- Sequential integer insert at N=1k now beats Ruby Hash (it lost slightly in 0.1.2).
|
|
75
|
+
- ARM64 uses the NEON group-matching path (`stats[:simd] => "NEON"`).
|
|
76
|
+
- Ruby's built-in `Hash` remains excellent, especially for very small maps and cases that benefit from VM-level Hash specialization.
|
|
68
77
|
|
|
69
78
|
### Memory Usage
|
|
70
79
|
|
|
71
|
-
For 100,000 integer keys:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
For 100,000 integer keys in the current benchmark:
|
|
81
|
+
|
|
82
|
+
| Implementation | Reported memory |
|
|
83
|
+
|---|---:|
|
|
84
|
+
| SwissHash | 2,176 KB native + 4 GC slots |
|
|
85
|
+
| Ruby Hash | 3 GC slots; native memory not directly measurable from this benchmark |
|
|
86
|
+
|
|
87
|
+
Additional stats: load factor 76.3%, max load factor 87.5%, SIMD path reported as NEON on the benchmarked Apple Silicon machine.
|
|
76
88
|
|
|
77
89
|
## Features
|
|
78
90
|
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
83
|
-
- **
|
|
91
|
+
- **Swiss Table probing**: 7-bit `H2` metadata, group probing, triangular probe sequence, and 87.5% max load factor.
|
|
92
|
+
- **SIMD group matching**: SSE2 on x86_64, NEON on aarch64, SWAR fallback elsewhere.
|
|
93
|
+
- **Fast string-key path**: wyhash for string keys, frozen string key preparation, 7-bit strings of different encodings hash/compare like Ruby Hash, and direct `memcmp` when encodings are compatible.
|
|
94
|
+
- **Low GC pressure**: keys and values are Ruby objects, while control bytes and slots live in contiguous native arrays.
|
|
95
|
+
- **Delete/reinsert friendly**: tombstones are tracked and compacted to avoid pathological slowdown.
|
|
96
|
+
- **Hash-like API**: basic accessors, enumeration, fetch helpers, merge/update/replace, filtering, transforming, slicing, inversion, and conversion helpers.
|
|
97
|
+
- **Native hot paths**: performance-critical methods are implemented in C; small convenience wrappers live in Ruby where that does not affect the core benchmark paths.
|
|
84
98
|
|
|
85
99
|
## API
|
|
86
100
|
|
|
@@ -89,96 +103,165 @@ hash = SwissHash::Hash.new(capacity = 16)
|
|
|
89
103
|
|
|
90
104
|
# Basic operations
|
|
91
105
|
hash[key] = value
|
|
92
|
-
hash
|
|
93
|
-
hash
|
|
106
|
+
hash.store(key, value)
|
|
107
|
+
hash[key] # returns nil if absent
|
|
108
|
+
hash.fetch(key)
|
|
109
|
+
hash.fetch(key, default)
|
|
110
|
+
hash.fetch(key) { |missing_key| ... }
|
|
111
|
+
hash.delete(key) # returns old value or nil
|
|
112
|
+
hash.clear
|
|
113
|
+
hash.replace(other_hash)
|
|
114
|
+
|
|
115
|
+
# Merge/update
|
|
116
|
+
hash.merge(other_hash)
|
|
117
|
+
hash.merge(other_hash) { |key, old_value, new_value| ... }
|
|
118
|
+
hash.merge!(other_hash)
|
|
119
|
+
hash.update(other_hash)
|
|
94
120
|
|
|
95
121
|
# Enumeration
|
|
96
|
-
hash.each { |
|
|
122
|
+
hash.each { |key, value| ... }
|
|
123
|
+
hash.each_pair { |key, value| ... }
|
|
124
|
+
hash.each_key { |key| ... }
|
|
125
|
+
hash.each_value { |value| ... }
|
|
97
126
|
hash.keys
|
|
98
127
|
hash.values
|
|
128
|
+
hash.to_a
|
|
99
129
|
|
|
100
|
-
#
|
|
101
|
-
hash.size
|
|
130
|
+
# Query helpers
|
|
131
|
+
hash.size # also: length
|
|
102
132
|
hash.empty?
|
|
103
|
-
hash.key?(key)
|
|
133
|
+
hash.key?(key) # also: has_key?, include?, member?
|
|
134
|
+
hash.value?(value) # also: has_value?
|
|
135
|
+
hash.key(value) # first key for value, or nil
|
|
136
|
+
hash.assoc(key)
|
|
137
|
+
hash.rassoc(value)
|
|
138
|
+
hash.values_at(*keys)
|
|
139
|
+
hash.fetch_values(*keys)
|
|
140
|
+
hash.dig(key, *path)
|
|
141
|
+
hash.count # Enumerable-compatible
|
|
142
|
+
|
|
143
|
+
# Filtering and transforms
|
|
144
|
+
hash.slice(*keys)
|
|
145
|
+
hash.except(*keys)
|
|
146
|
+
hash.select { |key, value| ... } # also: filter
|
|
147
|
+
hash.select! { |key, value| ... } # also: filter!
|
|
148
|
+
hash.reject { |key, value| ... }
|
|
149
|
+
hash.reject! { |key, value| ... }
|
|
150
|
+
hash.delete_if { |key, value| ... }
|
|
151
|
+
hash.keep_if { |key, value| ... }
|
|
152
|
+
hash.compact
|
|
153
|
+
hash.compact!
|
|
154
|
+
hash.transform_keys { |key| ... }
|
|
155
|
+
hash.transform_keys! { |key| ... }
|
|
156
|
+
hash.transform_values { |value| ... }
|
|
157
|
+
hash.transform_values! { |value| ... }
|
|
158
|
+
hash.invert
|
|
159
|
+
hash.shift
|
|
160
|
+
hash.flatten(level = 1)
|
|
161
|
+
|
|
162
|
+
# Conversion
|
|
163
|
+
hash.to_h # returns a Ruby Hash
|
|
164
|
+
hash.to_sh # returns a shallow SwissHash copy
|
|
165
|
+
|
|
166
|
+
# Maintenance / debugging
|
|
167
|
+
hash.compact_storage! # drop tombstones without changing values
|
|
168
|
+
hash.stats # => { capacity:, size:, num_groups:, load_factor:,
|
|
169
|
+
# memory_bytes:, growth_left:, tombstones:,
|
|
170
|
+
# simd:, layout: }
|
|
171
|
+
```
|
|
104
172
|
|
|
105
|
-
|
|
106
|
-
hash.clear
|
|
107
|
-
hash.compact! # drop tombstones without reallocating
|
|
173
|
+
### Compatibility notes
|
|
108
174
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
SwissHash aims to cover the practical subset of `Hash` that is useful for a fast native hash map, but it is not a drop-in replacement for every Ruby Hash semantic.
|
|
176
|
+
|
|
177
|
+
Not currently supported:
|
|
178
|
+
|
|
179
|
+
- default values and default blocks from `Hash.new(default)` / `Hash.new { ... }`
|
|
180
|
+
- `compare_by_identity`
|
|
181
|
+
- full insertion-order guarantees
|
|
182
|
+
- every rarely used method from Ruby's full `Hash` API
|
|
113
183
|
|
|
114
184
|
## Usage Recommendations
|
|
115
185
|
|
|
116
186
|
Use SwissHash when:
|
|
117
|
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
187
|
+
|
|
188
|
+
- keys are mostly **strings** and insert speed matters;
|
|
189
|
+
- the map commonly holds **10,000+ entries**;
|
|
190
|
+
- workloads include deletes and reinserts;
|
|
191
|
+
- predictable native memory layout and lower Ruby-object churn are useful.
|
|
121
192
|
|
|
122
193
|
Stick with Ruby's built-in `Hash` when:
|
|
123
|
-
|
|
124
|
-
-
|
|
194
|
+
|
|
195
|
+
- the hash is small and mostly lookup-heavy with integer keys;
|
|
196
|
+
- you depend on exact Ruby Hash semantics such as defaults, insertion order, `compare_by_identity`, or the complete standard API;
|
|
197
|
+
- the code path benefits from VM-level `Hash#[]` specialization more than from the underlying table layout.
|
|
125
198
|
|
|
126
199
|
## Architecture
|
|
127
200
|
|
|
128
201
|
### Swiss Table core
|
|
129
|
-
|
|
130
|
-
- **
|
|
131
|
-
- **
|
|
132
|
-
- **
|
|
202
|
+
|
|
203
|
+
- **Open addressing** with 7-bit `H2` metadata byte per slot; group matching rejects non-matching slots in batches.
|
|
204
|
+
- **Group size 16 on SSE2 (x86_64) and NEON (aarch64)**, **group size 8 on portable SWAR**. The active path is printed by `stats[:simd]` / the benchmark memory section (`NEON` on Apple Silicon).
|
|
205
|
+
- **Triangular probing** — `i(i+1)/2` — over power-of-two group counts.
|
|
206
|
+
- **Max load factor 87.5%** (7/8).
|
|
133
207
|
|
|
134
208
|
### Ruby-specific adaptations
|
|
135
|
-
|
|
136
|
-
- **
|
|
137
|
-
- **
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
- **
|
|
209
|
+
|
|
210
|
+
- **wyhash** for string keys.
|
|
211
|
+
- **Fibonacci multiplicative hash** for Fixnum and Symbol keys.
|
|
212
|
+
- **Frozen string key preparation** to avoid later key mutation surprises.
|
|
213
|
+
- **ASCII-7bit and encoding-index equality fast paths** before falling back to Ruby-compatible string comparison.
|
|
214
|
+
- **Inline `RTYPEDDATA_DATA`** on hot methods (`[]`, `[]=`, `delete`, `key?`) to avoid repeated typed-data checks.
|
|
215
|
+
- **Prefetch of slot groups** after control-byte load so data fetch overlaps with match extraction.
|
|
141
216
|
|
|
142
217
|
### Memory layout
|
|
143
|
-
|
|
144
|
-
-
|
|
218
|
+
|
|
219
|
+
- One native allocation for control bytes followed by slots (16-byte aligned). `stats[:memory_bytes]` still counts `capacity * (1 + sizeof(Slot))`.
|
|
220
|
+
- Native arrays are allocated outside Ruby's object heap; keys and values are still marked for GC.
|
|
221
|
+
- Slot memory is not zero-initialized on allocation; slots are read only after their control byte marks them live.
|
|
145
222
|
|
|
146
223
|
## Build
|
|
147
224
|
|
|
225
|
+
```bash
|
|
226
|
+
bundle install
|
|
227
|
+
bundle exec rake compile
|
|
148
228
|
```
|
|
149
|
-
|
|
229
|
+
|
|
230
|
+
## Test
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
bundle exec ruby test/hash_api_test.rb
|
|
234
|
+
bundle exec ruby test/string_key_mutation_test.rb
|
|
235
|
+
bundle exec ruby test/safety_and_encoding_test.rb
|
|
150
236
|
```
|
|
151
237
|
|
|
152
238
|
## Benchmarking
|
|
153
239
|
|
|
154
|
-
The included `benchmark.rb` produces statistically honest results:
|
|
155
|
-
|
|
156
240
|
```bash
|
|
157
241
|
bundle exec ruby benchmark.rb
|
|
158
242
|
```
|
|
159
243
|
|
|
160
|
-
|
|
161
|
-
- **Interleaved Ruby/SwissHash measurements** per iteration with alternating start order — thermal throttling and fluctuating background load hit both sides equally
|
|
162
|
-
- **21 iterations with IQR-filtered mean** (trims top and bottom 25%) — more robust than median on noisy laptop hardware
|
|
163
|
-
- **5 warmup runs** to settle JIT, caches, and branch predictor
|
|
164
|
-
- **Per-side coefficient of variation** (`±X.X%`) displayed so you can distinguish a real 5% delta from 5% noise
|
|
165
|
-
- **Correctness smoke test** runs before measurement
|
|
244
|
+
The benchmark includes a smoke test before timing and prints the active SIMD/SWAR path in the memory section.
|
|
166
245
|
|
|
167
246
|
### Profiling
|
|
168
247
|
|
|
169
248
|
For profiling on macOS:
|
|
170
249
|
|
|
171
250
|
```bash
|
|
172
|
-
bundle exec ruby simp.rb # runs infinite lookup loop
|
|
251
|
+
bundle exec ruby simp.rb # runs an infinite lookup loop and prints PID
|
|
173
252
|
sample <PID> 60 -f /tmp/swiss.sample
|
|
174
253
|
filtercalltree /tmp/swiss.sample | head -100
|
|
175
254
|
```
|
|
176
255
|
|
|
256
|
+
## Changelog
|
|
257
|
+
|
|
258
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
259
|
+
|
|
177
260
|
## Design References
|
|
178
261
|
|
|
179
262
|
- Matt Kulukundis, ["Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step"](https://www.youtube.com/watch?v=ncHmEUmJZf4) — CppCon 2017
|
|
180
263
|
- [Abseil: SwissTables design](https://abseil.io/about/design/swisstables)
|
|
181
|
-
- [rust-lang/hashbrown](https://github.com/rust-lang/hashbrown) — reference for SSE2/
|
|
264
|
+
- [rust-lang/hashbrown](https://github.com/rust-lang/hashbrown) — reference for SSE2/portable group strategy choices
|
|
182
265
|
- [Go 1.24 maps](https://go.dev/blog/swisstable) — probing and resize design trade-offs
|
|
183
266
|
- Aria Beingessner, ["Swisstable, a Quick and Dirty Description"](https://faultlore.com/blah/hashbrown-tldr/) — implementer's notes
|
|
184
267
|
|