swiss_hash 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9846e358fdd8020ef06ef05446b3684c86b658b2643a4301dab6fdd65c22aaa6
4
- data.tar.gz: 66e6452775731dc9eca53cfaa33c63cd434499452bc5c30c1924ca1414e6f27b
3
+ metadata.gz: bd83d549a02b9134baf31b4fa97b7eca44f370653cb1a3af0076f9d1196c1e15
4
+ data.tar.gz: 11a81f20822f58be2f18936e9a7e5bda112759868d91a8d7e2dcc1332bce598a
5
5
  SHA512:
6
- metadata.gz: a3b691649208e0eaada137990e286a319050cf5b64ed88802b94d2579020ddff519c77cb813e5d09e8b026c892581847131b65b246e431910ba61a3e6484eeef
7
- data.tar.gz: ffc257a83083611a0dc70fe9232c78a0ead1ef226956b887a1132e27e88d037d420977918c534f179a6598c0c2e55fadddc2340fb8c925a5ef0621d4c5798402
6
+ metadata.gz: 79009afb0d1ef895a975ac2ad10c612845f5e81e473e4476e9cb7a5732f482241a5ebb73427633aeecffaf3c516406e8f88c99c69d02da5db19c8ca1aa8db53d
7
+ data.tar.gz: 0736cadfdf1e688e4c47f29c120edc8c9eddc1602b9ab933fd740fd954db7af076ae1eea4f333028d2be8edf9e066ab7797a4dc1ec6faf91b7fd37a7f10f58bc
data/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 - Unreleased
4
+
5
+ Documentation and API polish for the first public release candidate.
6
+
7
+ ### Added / changed
8
+
9
+ - Expanded the Hash-like API surface:
10
+ - `fetch`, `values_at`, `fetch_values`
11
+ - `merge`, `merge!`, `update`, `replace`
12
+ - `to_h`, `to_sh`, `to_a`
13
+ - `slice`, `except`, `invert`, `assoc`, `rassoc`, `shift`
14
+ - `delete_if`, `keep_if`, `select`, `select!`, `filter`, `filter!`, `reject`, `reject!`
15
+ - `compact`, `compact!`, `transform_keys`, `transform_keys!`, `transform_values`, `transform_values!`
16
+ - `value?`, `has_value?`, `key`, `dig`, `count`, `flatten`, `==`, `eql?`, `hash`, `inspect`
17
+ - Moved the important Hash-like operations into the C extension instead of doing the expensive parts in Ruby.
18
+ - Kept `to_h` as the Ruby-compatible conversion to a real `Hash`; added `to_sh` as the explicit shallow SwissHash copy.
19
+ - Updated the README with the latest benchmark output from Ruby 3.4.3 / arm64-darwin24.
20
+ - 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.
21
+
22
+ ### Benchmark update
23
+
24
+ Struck-through values are the previous README numbers. The value after the arrow is the current benchmark result.
25
+
26
+ #### N = 100,000
27
+
28
+ | Operation | Ruby Hash | SwissHash | Delta |
29
+ |---|---:|---:|---:|
30
+ | Insert (string keys) | ~~17.6 ms~~ → 16.485 ms | ~~11.3 ms~~ → 10.386 ms | ~~−35.8%~~ → **−37.00%** |
31
+ | Delete + reinsert 25% | ~~9.8 ms~~ → 8.965 ms | ~~8.9 ms~~ → 7.164 ms | ~~−9.0%~~ → **−20.09%** |
32
+ | Insert (sequential int) | ~~7.0 ms~~ → 6.324 ms | ~~6.4 ms~~ → 5.023 ms | ~~−8.7%~~ → **−20.57%** |
33
+ | Mixed (70/20/10 R/W/D) | ~~21.4 ms~~ → 21.784 ms | ~~19.6 ms~~ → 18.990 ms | ~~−8.6%~~ → **−12.83%** |
34
+ | Insert (random int) | ~~6.7 ms~~ → 5.963 ms | ~~6.4 ms~~ → 5.010 ms | ~~−3.5%~~ → **−15.98%** |
35
+ | Lookup (string keys, 3x) | ~~20.3 ms~~ → 20.561 ms | ~~20.8 ms~~ → 21.535 ms | ~~+2.5%~~ → +4.74% |
36
+ | Lookup (sequential int, 3x) | ~~11.7 ms~~ → 13.281 ms | ~~12.2 ms~~ → 11.116 ms | ~~+4.4%~~ → **−16.31%** |
37
+
38
+ #### N = 10,000
39
+
40
+ | Operation | Ruby Hash | SwissHash | Delta |
41
+ |---|---:|---:|---:|
42
+ | Insert (string keys) | ~~1.63 ms~~ → 1.522 ms | ~~1.10 ms~~ → 0.992 ms | ~~−32.8%~~ → **−34.80%** |
43
+ | Lookup (string keys, 3x) | ~~1.71 ms~~ → 1.710 ms | ~~1.62 ms~~ → 1.563 ms | ~~−5.2%~~ → **−8.58%** |
44
+ | Mixed (70/20/10 R/W/D) | ~~1.93 ms~~ → 1.988 ms | ~~1.90 ms~~ → 1.869 ms | ~~−1.6%~~ → **−5.98%** |
45
+ | Delete + reinsert 25% | ~~0.91 ms~~ → 0.814 ms | ~~0.89 ms~~ → 0.714 ms | ~~−2.0%~~ → **−12.29%** |
46
+ | Insert (sequential int) | ~~0.66 ms~~ → 0.578 ms | ~~0.67 ms~~ → 0.510 ms | ~~+2.5%~~ → **−11.82%** |
47
+ | Lookup (sequential int, 3x) | ~~1.05 ms~~ → 1.071 ms | ~~1.12 ms~~ → 1.065 ms | ~~+6.0%~~ → −0.55% |
48
+ | Insert (random int) | previously not listed → 0.554 ms | previously not listed → 0.501 ms | **−9.53%** |
49
+
50
+ #### N = 1,000
51
+
52
+ | Operation | Ruby Hash | SwissHash | Delta |
53
+ |---|---:|---:|---:|
54
+ | Insert (string keys) | ~~0.183 ms~~ → 0.156 ms | ~~0.118 ms~~ → 0.102 ms | ~~−35.6%~~ → **−34.78%** |
55
+ | Lookup (string keys, 3x) | ~~0.184 ms~~ → 0.181 ms | ~~0.155 ms~~ → 0.150 ms | ~~−15.5%~~ → **−17.10%** |
56
+ | Insert (sequential int) | ~~0.063 ms~~ → 0.056 ms | ~~0.074 ms~~ → 0.057 ms | ~~+17.5%~~ → +1.88% |
57
+ | Delete + reinsert 25% | ~~0.094 ms~~ → 0.082 ms | ~~0.102 ms~~ → 0.079 ms | ~~+8.2%~~ → −4.12% |
58
+ | Insert (random int) | previously not listed → 0.054 ms | previously not listed → 0.051 ms | −6.54% |
59
+ | Lookup (sequential int, 3x) | previously not listed → 0.111 ms | previously not listed → 0.111 ms | +0.29% |
60
+ | Mixed (70/20/10 R/W/D) | previously not listed → 0.202 ms | previously not listed → 0.197 ms | −2.04% |
61
+
62
+ ### Memory update
63
+
64
+ | Metric | Previous README | Current benchmark |
65
+ |---|---:|---:|
66
+ | SwissHash native memory, N=100,000 | ~~2,176 KB + 4 GC slots~~ | 2,176 KB native + 4 GC slots |
67
+ | Ruby Hash | ~~managed via GC slots / not directly measurable~~ | 3 GC slots; native memory not directly measurable |
68
+ | Load factor | ~~76.3%~~ | 76.3% |
69
+ | SIMD path | not shown in table | SWAR |
70
+
71
+ ## 0.1.1 and earlier
72
+
73
+ 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/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 Roman Haidarov
3
+ Copyright (c) 2026 Roman Haidarov
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # SwissHash
2
2
 
3
- Swiss Table hash map implementation as a Ruby C extension. Based on Go 1.24 Swiss Table design with SIMD support (SSE2/NEON).
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,42 +15,85 @@ require "swiss_hash"
15
15
 
16
16
  h = SwissHash::Hash.new
17
17
  h["key"] = "value"
18
- h["key"] # => "value"
19
- h.delete("key")
20
- h.stats # => { capacity: 16, size: 0, ... }
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.1.7 / arm64-darwin24 with NEON SIMD support (median of 3 runs):
28
+ Benchmarks below were produced by `benchmark.rb` on Ruby 3.4.3 / arm64-darwin24.
29
+
30
+ Methodology: 10 runs × 21 measured iterations, 5 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.
31
+
32
+ ### N = 100,000
33
+
34
+ | Operation | Ruby Hash | SwissHash | Delta |
35
+ |---|---:|---:|---:|
36
+ | Insert (sequential int) | 6.324 ms (±0.5%) | 5.023 ms (±0.6%) | **−20.57%** ⚡ |
37
+ | Insert (string keys) | 16.485 ms (±4.6%) | 10.386 ms (±3.0%) | **−37.00%** ⚡ |
38
+ | Insert (random int) | 5.963 ms (±1.2%) | 5.010 ms (±1.0%) | **−15.98%** ⚡ |
39
+ | Lookup (sequential int, 3x) | 13.281 ms (±0.1%) | 11.116 ms (±0.1%) | **−16.31%** ⚡ |
40
+ | Lookup (string keys, 3x) | 20.561 ms (±3.8%) | 21.535 ms (±4.9%) | +4.74% |
41
+ | Delete + reinsert 25% | 8.965 ms (±0.7%) | 7.164 ms (±0.8%) | **−20.09%** ⚡ |
42
+ | Mixed (70% read / 20% write / 10% delete) | 21.784 ms (±0.2%) | 18.990 ms (±0.3%) | **−12.83%** ⚡ |
43
+
44
+ ### N = 10,000
45
+
46
+ | Operation | Ruby Hash | SwissHash | Delta |
47
+ |---|---:|---:|---:|
48
+ | Insert (sequential int) | 0.578 ms (±1.1%) | 0.510 ms (±1.1%) | **−11.82%** ⚡ |
49
+ | Insert (string keys) | 1.522 ms (±3.5%) | 0.992 ms (±1.6%) | **−34.80%** ⚡ |
50
+ | Insert (random int) | 0.554 ms (±2.0%) | 0.501 ms (±2.5%) | **−9.53%** ⚡ |
51
+ | Lookup (sequential int, 3x) | 1.071 ms (±0.3%) | 1.065 ms (±0.1%) | −0.55% |
52
+ | Lookup (string keys, 3x) | 1.710 ms (±1.3%) | 1.563 ms (±1.4%) | **−8.58%** ⚡ |
53
+ | Delete + reinsert 25% | 0.814 ms (±1.6%) | 0.714 ms (±1.2%) | **−12.29%** ⚡ |
54
+ | Mixed (70% read / 20% write / 10% delete) | 1.988 ms (±0.4%) | 1.869 ms (±0.5%) | **−5.98%** ⚡ |
55
+
56
+ ### N = 1,000
26
57
 
27
- ### Insert Performance
28
- - **String keys**: SwissHash is **18.9-28.6% faster** across all dataset sizes
29
- - **Sequential integers**: 24.6% slower (1k), 3.7% slower (10k), **11.5% faster** (100k)
30
- - **Random integers**: 11.6% slower (1k), 4.7% slower (10k), **6.3% faster** (100k)
58
+ Ruby Hash uses an AR-table for small hashes, so very small integer-keyed workloads can still favour the built-in implementation. String-heavy workloads continue to be the strongest SwissHash case.
31
59
 
32
- ### Lookup Performance
33
- - **Sequential integers**: SwissHash is **7.3-10.6% slower** across all sizes
34
- - **String keys**: SwissHash is **13.0-25.2% slower** across all sizes
60
+ | Operation | Ruby Hash | SwissHash | Delta |
61
+ |---|---:|---:|---:|
62
+ | Insert (sequential int) | 0.056 ms (±0.7%) | 0.057 ms (±0.9%) | +1.88% |
63
+ | Insert (string keys) | 0.156 ms (±2.5%) | 0.102 ms (±0.9%) | **−34.78%** ⚡ |
64
+ | Insert (random int) | 0.054 ms (±3.5%) | 0.051 ms (±3.4%) | −6.54% |
65
+ | Lookup (sequential int, 3x) | 0.111 ms (±0.4%) | 0.111 ms (±0.2%) | +0.29% |
66
+ | Lookup (string keys, 3x) | 0.181 ms (±0.3%) | 0.150 ms (±0.4%) | **−17.10%** ⚡ |
67
+ | Delete + reinsert 25% | 0.082 ms (±0.9%) | 0.079 ms (±0.5%) | −4.12% |
68
+ | Mixed (70% read / 20% write / 10% delete) | 0.202 ms (±0.3%) | 0.197 ms (±0.2%) | −2.04% |
35
69
 
36
- ### Mixed Workloads
37
- - **Delete+reinsert (25%)**: 9.1% slower (1k), 2.5% slower (10k), **13.5% faster** (100k)
38
- - **Mixed operations (70% read, 20% write, 10% delete)**: 2.0% slower (1k), 0.5% faster (10k), **11.6% faster** (100k)
70
+ ### Summary
71
+
72
+ - SwissHash is faster on **6 of 7 operations** at N=100k in the current benchmark run.
73
+ - The strongest win is still string-key insertion: **−34% to −37%** across tested sizes.
74
+ - Large integer-keyed inserts, delete/reinsert churn, and mixed workloads improved substantially after moving more Hash-like operations into C and keeping the hot paths lean.
75
+ - String lookups are workload-sensitive: SwissHash wins at N=1k and N=10k, while the N=100k run is slightly slower than Ruby Hash within a noisier test band.
76
+ - Ruby's built-in `Hash` remains excellent, especially for very small maps and cases that benefit from VM-level Hash specialization.
39
77
 
40
78
  ### Memory Usage
41
- For 100,000 elements:
42
- - **SwissHash**: 2,176 KB native memory + 4 GC slots
43
- - **Ruby Hash**: 3 GC slots (native memory not directly measurable)
44
- - **Load factor**: 76.3% (efficient memory usage)
45
- - **GC pressure**: Equal (0 GC runs during insertion)
79
+
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 SWAR on the benchmark machine.
46
88
 
47
89
  ## Features
48
90
 
49
- - **SIMD optimized**: Uses SSE2 on x86_64 and NEON on ARM64
50
- - **Memory efficient**: Swiss Table layout with 87.5% max load factor
51
- - **Tombstone compaction**: Automatic cleanup of deleted entries
52
- - **Ruby compatibility**: Supports frozen string keys, all Ruby object types
53
- - **Thread safety**: Prevents reentrant modifications during callbacks
91
+ - **Swiss Table probing**: 7-bit `H2` metadata, group probing, triangular probe sequence, and 87.5% max load factor.
92
+ - **Fast string-key path**: wyhash for string keys, frozen string key preparation, ASCII-7bit equality shortcut, and direct `memcmp` when encodings are compatible.
93
+ - **Low GC pressure**: keys and values are Ruby objects, while control bytes and slots live in contiguous native arrays.
94
+ - **Delete/reinsert friendly**: tombstones are tracked and compacted to avoid pathological slowdown.
95
+ - **Hash-like API**: basic accessors, enumeration, fetch helpers, merge/update/replace, filtering, transforming, slicing, inversion, and conversion helpers.
96
+ - **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.
54
97
 
55
98
  ## API
56
99
 
@@ -59,83 +102,166 @@ hash = SwissHash::Hash.new(capacity = 16)
59
102
 
60
103
  # Basic operations
61
104
  hash[key] = value
62
- hash[key] # get
63
- hash.delete(key) # delete, returns old value or nil
105
+ hash.store(key, value)
106
+ hash[key] # returns nil if absent
107
+ hash.fetch(key)
108
+ hash.fetch(key, default)
109
+ hash.fetch(key) { |missing_key| ... }
110
+ hash.delete(key) # returns old value or nil
111
+ hash.clear
112
+ hash.replace(other_hash)
113
+
114
+ # Merge/update
115
+ hash.merge(other_hash)
116
+ hash.merge(other_hash) { |key, old_value, new_value| ... }
117
+ hash.merge!(other_hash)
118
+ hash.update(other_hash)
64
119
 
65
120
  # Enumeration
66
- hash.each { |k, v| ... }
121
+ hash.each { |key, value| ... }
122
+ hash.each_pair { |key, value| ... }
123
+ hash.each_key { |key| ... }
124
+ hash.each_value { |value| ... }
67
125
  hash.keys
68
126
  hash.values
127
+ hash.to_a
69
128
 
70
- # Size and status
71
- hash.size
129
+ # Query helpers
130
+ hash.size # also: length
72
131
  hash.empty?
73
- hash.key?(key)
132
+ hash.key?(key) # also: has_key?, include?, member?
133
+ hash.value?(value) # also: has_value?
134
+ hash.key(value) # first key for value, or nil
135
+ hash.assoc(key)
136
+ hash.rassoc(value)
137
+ hash.values_at(*keys)
138
+ hash.fetch_values(*keys)
139
+ hash.dig(key, *path)
140
+ hash.count # Enumerable-compatible
141
+
142
+ # Filtering and transforms
143
+ hash.slice(*keys)
144
+ hash.except(*keys)
145
+ hash.select { |key, value| ... } # also: filter
146
+ hash.select! { |key, value| ... } # also: filter!
147
+ hash.reject { |key, value| ... }
148
+ hash.reject! { |key, value| ... }
149
+ hash.delete_if { |key, value| ... }
150
+ hash.keep_if { |key, value| ... }
151
+ hash.compact
152
+ hash.compact!
153
+ hash.transform_keys { |key| ... }
154
+ hash.transform_keys! { |key| ... }
155
+ hash.transform_values { |value| ... }
156
+ hash.transform_values! { |value| ... }
157
+ hash.invert
158
+ hash.shift
159
+ hash.flatten(level = 1)
160
+
161
+ # Conversion
162
+ hash.to_h # returns a Ruby Hash
163
+ hash.to_sh # returns a shallow SwissHash copy
164
+
165
+ # Maintenance / debugging
166
+ hash.compact_storage! # drop tombstones without changing values
167
+ hash.stats # => { capacity:, size:, num_groups:, load_factor:,
168
+ # memory_bytes:, growth_left:, tombstones:,
169
+ # simd:, layout: }
170
+ ```
74
171
 
75
- # Maintenance
76
- hash.clear
77
- hash.compact! # remove tombstones
172
+ ### Compatibility notes
78
173
 
79
- # Debugging
80
- hash.stats # => detailed statistics hash
81
- ```
174
+ 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.
175
+
176
+ Not currently supported:
177
+
178
+ - default values and default blocks from `Hash.new(default)` / `Hash.new { ... }`
179
+ - `compare_by_identity`
180
+ - full insertion-order guarantees
181
+ - every rarely used method from Ruby's full `Hash` API
82
182
 
83
183
  ## Usage Recommendations
84
184
 
85
- SwissHash shows clear advantages in specific scenarios:
86
- - **Heavy string key insertion** into any size hash table (consistent 19-29% improvement)
87
- - **Large dataset operations** (>100k elements) - shows improvements across most operations
88
- - **Write-heavy workloads** with frequent deletion and reinsertion patterns
89
- - **Cases where predictable memory usage is important**
185
+ Use SwissHash when:
186
+
187
+ - keys are mostly **strings** and insert speed matters;
188
+ - the map commonly holds **10,000+ entries**;
189
+ - workloads include deletes and reinserts;
190
+ - predictable native memory layout and lower Ruby-object churn are useful.
191
+
192
+ Stick with Ruby's built-in `Hash` when:
90
193
 
91
- ⚠️ **Important**: SwissHash is consistently **7-25% slower for all lookup operations**. For read-intensive applications, stick with standard Ruby Hash.
194
+ - the hash is small and mostly lookup-heavy with integer keys;
195
+ - you depend on exact Ruby Hash semantics such as defaults, insertion order, `compare_by_identity`, or the complete standard API;
196
+ - the code path benefits from VM-level `Hash#[]` specialization more than from the underlying table layout.
92
197
 
93
- ## Technical Analysis: Why Ruby Hash is Faster for Lookups
198
+ ## Architecture
94
199
 
95
- The performance difference in lookup operations comes from several architectural factors:
200
+ ### Swiss Table core
96
201
 
97
- ### Ruby VM Optimizations
98
- - **Specialized fast paths**: Ruby VM has highly optimized inline implementations for common key types (Fixnum, Symbol, String)
99
- - **Method caching**: VM-level inline caching for hash access patterns reduces method dispatch overhead
100
- - **Bytecode optimizations**: Hash access is optimized at the bytecode level with specialized opcodes
101
- - **Memory locality**: Ruby Hash uses a simpler layout optimized for CPU cache performance
202
+ - **Open addressing** with 7-bit `H2` metadata byte per slot; group matching rejects non-matching slots in batches.
203
+ - **Group size 16 on SSE2** and **group size 8 on portable SWAR**. On the benchmarked Apple Silicon machine the active path is SWAR.
204
+ - **Triangular probing** `i(i+1)/2` over power-of-two group counts.
205
+ - **Max load factor 87.5%** (7/8).
102
206
 
103
- ### SwissHash Overhead
104
- - **C Extension boundaries**: Each lookup requires `TypedData_Get_Struct()` call and Ruby-C interface overhead
105
- - **Complex hash computation**: SwissHash uses sophisticated hashing (wyhash for strings, custom mixers) vs Ruby's simpler approach
106
- - **Two-level lookup**: Swiss Table's H1/H2 split requires additional bit manipulation and SIMD operations
107
- - **SIMD overhead**: NEON/SSE2 operations have setup costs that don't pay off for small group scans
108
- - **Additional equality checks**: `keys_equal()` function adds extra indirection compared to VM's direct comparison
207
+ ### Ruby-specific adaptations
109
208
 
110
- ### Architecture Trade-offs
111
- - **Swiss Table design**: Optimized for high load factors and cache efficiency, but adds complexity to the critical lookup path
112
- - **Group-based probing**: While theoretically faster, the overhead of SIMD operations and multiple memory accesses hurts performance for typical Ruby workloads
113
- - **Memory indirection**: SwissHash's separate control bytes array creates additional memory accesses vs Ruby Hash's integrated approach
209
+ - **wyhash** for string keys.
210
+ - **Fibonacci multiplicative hash** for Fixnum and Symbol keys.
211
+ - **Frozen string key preparation** to avoid later key mutation surprises.
212
+ - **ASCII-7bit and encoding-index equality fast paths** before falling back to Ruby-compatible string comparison.
213
+ - **Inline `RTYPEDDATA_DATA`** on hot methods (`[]`, `[]=`, `delete`, `key?`) to avoid repeated typed-data checks.
214
+ - **Prefetch of slot groups** after control-byte load so data fetch overlaps with match extraction.
114
215
 
115
- This explains why SwissHash excels at write-heavy operations (where its design advantages matter) but loses to Ruby's VM-optimized lookups.
216
+ ### Memory layout
217
+
218
+ - Separate control-byte array and slot array.
219
+ - Native arrays are allocated outside Ruby's object heap; keys and values are still marked for GC.
220
+ - Slot memory is not zero-initialized on allocation; slots are read only after their control byte marks them live.
116
221
 
117
222
  ## Build
118
223
 
224
+ ```bash
225
+ bundle install
226
+ bundle exec rake compile
119
227
  ```
120
- rake compile
228
+
229
+ ## Test
230
+
231
+ ```bash
232
+ bundle exec ruby test/hash_api_test.rb
233
+ bundle exec ruby test/string_key_mutation_test.rb
234
+ ```
235
+
236
+ ## Benchmarking
237
+
238
+ ```bash
239
+ bundle exec ruby benchmark.rb
121
240
  ```
122
241
 
123
- ## Analysis Tools
242
+ The benchmark includes a smoke test before timing and prints the active SIMD/SWAR path in the memory section.
243
+
244
+ ### Profiling
124
245
 
125
- Project includes comprehensive benchmark with statistical validation:
246
+ For profiling on macOS:
126
247
 
127
248
  ```bash
128
- ruby -Ilib benchmark.rb
249
+ bundle exec ruby simp.rb # runs an infinite lookup loop and prints PID
250
+ sample <PID> 60 -f /tmp/swiss.sample
251
+ filtercalltree /tmp/swiss.sample | head -100
129
252
  ```
130
253
 
131
- The benchmark measures (with 7 iterations, 2 warmup, GC disabled):
132
- - Insert performance (sequential int, strings, random int)
133
- - Lookup performance (with 3x repetition for signal amplification)
134
- - Delete with reinsertion operations
135
- - Mixed workloads (70/20/10 read/write/delete)
136
- - Memory usage and GC pressure analysis
254
+ ## Changelog
255
+
256
+ See [CHANGELOG.md](CHANGELOG.md).
257
+
258
+ ## Design References
137
259
 
138
- Results are statistically validated through multiple runs to ensure accuracy.
260
+ - Matt Kulukundis, ["Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step"](https://www.youtube.com/watch?v=ncHmEUmJZf4) — CppCon 2017
261
+ - [Abseil: SwissTables design](https://abseil.io/about/design/swisstables)
262
+ - [rust-lang/hashbrown](https://github.com/rust-lang/hashbrown) — reference for SSE2/portable group strategy choices
263
+ - [Go 1.24 maps](https://go.dev/blog/swisstable) — probing and resize design trade-offs
264
+ - Aria Beingessner, ["Swisstable, a Quick and Dirty Description"](https://faultlore.com/blah/hashbrown-tldr/) — implementer's notes
139
265
 
140
266
  ## License
141
267