swiss_hash 0.1.2 → 0.1.4

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: bd83d549a02b9134baf31b4fa97b7eca44f370653cb1a3af0076f9d1196c1e15
4
- data.tar.gz: 11a81f20822f58be2f18936e9a7e5bda112759868d91a8d7e2dcc1332bce598a
3
+ metadata.gz: 0c2f1e2b2c9e7a8ee0ba2b4ab30f7088eeeefdd1e56abc6fb92758bd5a667a64
4
+ data.tar.gz: 92904e400f7f16702a48c2966afc8c41b4e05aacd769fbd3ff9abd54d8cd5b53
5
5
  SHA512:
6
- metadata.gz: 79009afb0d1ef895a975ac2ad10c612845f5e81e473e4476e9cb7a5732f482241a5ebb73427633aeecffaf3c516406e8f88c99c69d02da5db19c8ca1aa8db53d
7
- data.tar.gz: 0736cadfdf1e688e4c47f29c120edc8c9eddc1602b9ab933fd740fd954db7af076ae1eea4f333028d2be8edf9e066ab7797a4dc1ec6faf91b7fd37a7f10f58bc
6
+ metadata.gz: e4a90125a2c8d4792299a7899331f14a0f285cb15fe497a5c53258641846f5811d8f07127cee8a3c0f698f35011a6a45190deeaa9149642fa36856ca04fc12fd
7
+ data.tar.gz: d93013782fc2df669c537d4f0907c2c398781ddf077afd79dc0ddce455f18476ba2e0dc798dd09637c21cb866eb4ceed06c63669b8e625c5f26c5ae48633ab55
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 - 2026-10-28
4
+
5
+ - Prevented reentrant Fixnum insertion and `clear`/`initialize` from modifying a table during hash callbacks.
6
+ - Made reinitialization preserve the existing table if capacity validation or allocation fails; rejected overflowing capacities.
7
+ - Made `replace` with itself a no-op and kept contents intact when the replacement cannot convert to a Hash.
8
+ - Updated benchmark memory reporting to compare container storage and directly retained keys/values for Ruby Hash and SwissHash.
9
+ - Refreshed README timings and memory results for Ruby 3.4.3 / arm64-darwin24.
10
+
11
+ ## 0.1.3 - 2026-08-18
12
+
13
+ - NEON group matching on ARM64 (SSE2 on x86_64, SWAR elsewhere)
14
+ - Single allocation for control bytes and slots
15
+ - Faster Fixnum `[]` / `[]=`
16
+ - 7-bit strings of different encodings hash like Ruby Hash (`"abc"` and `"abc".b` are the same key)
17
+ - Rehash is GC-safe; `#hash` / `#eql?` that mutate the same table raise instead of corrupting it
18
+ - `initialize` no longer leaks on reuse; OOM no longer leaves a half-built table
19
+
3
20
  ## 0.1.2 - Unreleased
4
21
 
5
22
  Documentation and API polish for the first public release candidate.
data/README.md CHANGED
@@ -33,63 +33,63 @@ Methodology: 10 runs × 21 measured iterations, 5 warmup iterations per run, IQR
33
33
 
34
34
  | Operation | Ruby Hash | SwissHash | Delta |
35
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%** ⚡ |
36
+ | Insert (sequential int) | 6.412 ms (±0.5%) | 4.973 ms (±0.7%) | **−22.44%** ⚡ |
37
+ | Insert (string keys) | 16.130 ms (±0.2%) | 10.470 ms (±0.2%) | **−35.09%** ⚡ |
38
+ | Insert (random int) | 5.994 ms (±0.6%) | 4.763 ms (±0.8%) | **−20.55%** ⚡ |
39
+ | Lookup (sequential int, 3x) | 12.204 ms (±0.1%) | 11.050 ms (±0.1%) | **−9.46%** ⚡ |
40
+ | Lookup (string keys, 3x) | 20.465 ms (±0.2%) | 20.322 ms (±0.2%) | **−0.70%** ⚡ |
41
+ | Delete + reinsert 25% | 8.968 ms (±0.3%) | 7.075 ms (±0.4%) | **−21.10%** ⚡ |
42
+ | Mixed (70/20/10 R/W/D) | 22.168 ms (±0.3%) | 18.818 ms (±0.1%) | **−15.11%** ⚡ |
43
43
 
44
44
  ### N = 10,000
45
45
 
46
46
  | Operation | Ruby Hash | SwissHash | Delta |
47
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%** ⚡ |
48
+ | Insert (sequential int) | 0.577 ms (±1.2%) | 0.499 ms (±1.5%) | **−13.58%** ⚡ |
49
+ | Insert (string keys) | 1.479 ms (±1.6%) | 1.001 ms (±1.3%) | **−32.30%** ⚡ |
50
+ | Insert (random int) | 0.541 ms (±1.9%) | 0.470 ms (±2.2%) | **−13.10%** ⚡ |
51
+ | Lookup (sequential int, 3x) | 1.069 ms (±0.2%) | 1.035 ms (±0.2%) | **−3.22%** ⚡ |
52
+ | Lookup (string keys, 3x) | 1.692 ms (±0.4%) | 1.550 ms (±0.4%) | **−8.42%** ⚡ |
53
+ | Delete + reinsert 25% | 0.808 ms (±0.9%) | 0.701 ms (±0.7%) | **−13.33%** ⚡ |
54
+ | Mixed (70/20/10 R/W/D) | 1.979 ms (±0.6%) | 1.829 ms (±0.8%) | **−7.62%** ⚡ |
55
55
 
56
56
  ### N = 1,000
57
57
 
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.
59
-
60
58
  | Operation | Ruby Hash | SwissHash | Delta |
61
59
  |---|---:|---:|---:|
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% |
60
+ | Insert (sequential int) | 0.057 ms (±0.8%) | 0.053 ms (±1.3%) | **−6.53%** ⚡ |
61
+ | Insert (string keys) | 0.154 ms (±2.7%) | 0.103 ms (±0.9%) | **−32.69%** ⚡ |
62
+ | Insert (random int) | 0.054 ms (±1.4%) | 0.049 ms (±1.1%) | **−9.27%** ⚡ |
63
+ | Lookup (sequential int, 3x) | 0.110 ms (±0.5%) | 0.106 ms (±0.4%) | **−3.73%** ⚡ |
64
+ | Lookup (string keys, 3x) | 0.177 ms (±0.5%) | 0.151 ms (±0.4%) | **−14.37%** ⚡ |
65
+ | Delete + reinsert 25% | 0.081 ms (±0.8%) | 0.074 ms (±1.1%) | **−8.42%** ⚡ |
66
+ | Mixed (70/20/10 R/W/D) | 0.203 ms (±0.2%) | 0.191 ms (±0.3%) | **−5.88%** ⚡ |
69
67
 
70
68
  ### Summary
71
69
 
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.
70
+ - SwissHash is faster on all seven listed operations at N=100k; string lookup is within 1% of Ruby Hash.
71
+ - The strongest win is string-key insertion: **−32% to −35%** across tested sizes.
72
+ - ARM64 uses the NEON group-matching path (`stats[:simd] => "NEON"`).
76
73
  - Ruby's built-in `Hash` remains excellent, especially for very small maps and cases that benefit from VM-level Hash specialization.
77
74
 
78
75
  ### Memory Usage
79
76
 
80
- For 100,000 integer keys in the current benchmark:
77
+ For 100,000 entries, `ObjectSpace.memsize_of` reports the hash object/table and directly retained key/value objects. Integer keys and values are immediate values, so they add no separate object bytes. For string-key tests, stored string keys are counted; the shared input array and source strings are excluded from both totals.
81
78
 
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 |
79
+ | Workload | Implementation | Container | Referenced keys/values | Total |
80
+ |---|---|---:|---:|---:|
81
+ | Integer keys and values | SwissHash | 2,176.1 KiB | 0.0 KiB | 2,176.1 KiB |
82
+ | Integer keys and values | Ruby Hash | 4,096.2 KiB | 0.0 KiB | 4,096.2 KiB |
83
+ | String keys and integer values | SwissHash | 2,176.1 KiB | 6,408.1 KiB | 8,584.2 KiB |
84
+ | String keys and integer values | Ruby Hash | 4,096.2 KiB | 6,408.1 KiB | 10,504.3 KiB |
86
85
 
87
- Additional stats: load factor 76.3%, max load factor 87.5%, SIMD path reported as SWAR on the benchmark machine.
86
+ SwissHash uses 46.87% less measured memory for integer entries and 18.28% less for string-key entries. The measurement excludes allocator fragmentation and objects reachable only through nested values. Load factor: 76.3%; max load factor: 87.5%; SIMD path: NEON.
88
87
 
89
88
  ## Features
90
89
 
91
90
  - **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.
91
+ - **SIMD group matching**: SSE2 on x86_64, NEON on aarch64, SWAR fallback elsewhere.
92
+ - **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.
93
93
  - **Low GC pressure**: keys and values are Ruby objects, while control bytes and slots live in contiguous native arrays.
94
94
  - **Delete/reinsert friendly**: tombstones are tracked and compacted to avoid pathological slowdown.
95
95
  - **Hash-like API**: basic accessors, enumeration, fetch helpers, merge/update/replace, filtering, transforming, slicing, inversion, and conversion helpers.
@@ -200,7 +200,7 @@ Stick with Ruby's built-in `Hash` when:
200
200
  ### Swiss Table core
201
201
 
202
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.
203
+ - **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).
204
204
  - **Triangular probing** — `i(i+1)/2` — over power-of-two group counts.
205
205
  - **Max load factor 87.5%** (7/8).
206
206
 
@@ -215,7 +215,7 @@ Stick with Ruby's built-in `Hash` when:
215
215
 
216
216
  ### Memory layout
217
217
 
218
- - Separate control-byte array and slot array.
218
+ - One native allocation for control bytes followed by slots (16-byte aligned). `stats[:memory_bytes]` still counts `capacity * (1 + sizeof(Slot))`.
219
219
  - Native arrays are allocated outside Ruby's object heap; keys and values are still marked for GC.
220
220
  - Slot memory is not zero-initialized on allocation; slots are read only after their control byte marks them live.
221
221
 
@@ -231,6 +231,7 @@ bundle exec rake compile
231
231
  ```bash
232
232
  bundle exec ruby test/hash_api_test.rb
233
233
  bundle exec ruby test/string_key_mutation_test.rb
234
+ bundle exec ruby test/safety_and_encoding_test.rb
234
235
  ```
235
236
 
236
237
  ## Benchmarking
@@ -146,36 +146,77 @@ static inline uint64_t wyhash(const void *data, size_t len, uint64_t seed) {
146
146
  #if defined(__x86_64__) || defined(_M_X64)
147
147
  #define SWISS_USE_SSE2 1
148
148
  #include <emmintrin.h>
149
-
150
149
  #define GROUP_SIZE 16
151
- #define GROUP_MASK 0xFFFFu
150
+ #elif defined(__aarch64__) || defined(_M_ARM64)
151
+ #define SWISS_USE_NEON 1
152
+ #include <arm_neon.h>
153
+ #define GROUP_SIZE 16
152
154
  #else
153
155
  #define SWISS_USE_SWAR 1
154
156
  #define GROUP_SIZE 8
155
- #define GROUP_MASK 0xFFu
157
+ #endif
158
+
159
+ #if defined(SWISS_USE_SSE2) || defined(SWISS_USE_NEON)
160
+ #define SWISS_USE_VEC 1
156
161
  #endif
157
162
 
158
163
  #ifdef SWISS_USE_SSE2
159
164
 
160
- static inline __m128i ctrl_load(const uint8_t *ctrl) {
165
+ typedef __m128i ctrl_vec_t;
166
+
167
+ SH_ALWAYS_INLINE ctrl_vec_t ctrl_load(const uint8_t *ctrl) {
161
168
  return _mm_loadu_si128((const __m128i *)ctrl);
162
169
  }
163
170
 
164
- static inline uint32_t ctrl_match_h2_vec(__m128i cv, uint8_t h2) {
171
+ SH_ALWAYS_INLINE uint32_t ctrl_match_h2_vec(ctrl_vec_t cv, uint8_t h2) {
165
172
  __m128i cmp = _mm_cmpeq_epi8(cv, _mm_set1_epi8((char)h2));
166
173
  return (uint32_t)_mm_movemask_epi8(cmp);
167
174
  }
168
175
 
169
- static inline uint32_t ctrl_match_empty_vec(__m128i cv) {
176
+ SH_ALWAYS_INLINE uint32_t ctrl_match_empty_vec(ctrl_vec_t cv) {
170
177
  __m128i cmp = _mm_cmpeq_epi8(cv, _mm_set1_epi8((char)CTRL_EMPTY));
171
178
  return (uint32_t)_mm_movemask_epi8(cmp);
172
179
  }
173
180
 
174
- static inline uint32_t ctrl_match_empty_or_deleted_vec(__m128i cv) {
181
+ SH_ALWAYS_INLINE uint32_t ctrl_match_empty_or_deleted_vec(ctrl_vec_t cv) {
175
182
  return (uint32_t)_mm_movemask_epi8(cv);
176
183
  }
177
184
 
178
- static inline uint32_t ctrl_match_empty(const uint8_t *ctrl) {
185
+ SH_ALWAYS_INLINE uint32_t ctrl_match_empty(const uint8_t *ctrl) {
186
+ return ctrl_match_empty_vec(ctrl_load(ctrl));
187
+ }
188
+
189
+ #elif defined(SWISS_USE_NEON)
190
+
191
+ typedef uint8x16_t ctrl_vec_t;
192
+ typedef uint64_t ctrl_mask_t;
193
+
194
+ #define CTRL_MASK_SLOT(m) ((int)(ctz64(m) >> 2))
195
+ #define NEON_NIBBLE_MASK 0x1111111111111111ULL
196
+
197
+ SH_ALWAYS_INLINE ctrl_mask_t neon_mask64(uint8x16_t cmp) {
198
+ /* 4 bits per slot (0x0 / 0xF); keep one bit per nibble so mask &= mask-1 works. */
199
+ uint64_t packed = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(cmp), 4)), 0);
200
+ return packed & NEON_NIBBLE_MASK;
201
+ }
202
+
203
+ SH_ALWAYS_INLINE ctrl_vec_t ctrl_load(const uint8_t *ctrl) {
204
+ return vld1q_u8(ctrl);
205
+ }
206
+
207
+ SH_ALWAYS_INLINE ctrl_mask_t ctrl_match_h2_vec(ctrl_vec_t cv, uint8_t h2) {
208
+ return neon_mask64(vceqq_u8(cv, vdupq_n_u8(h2)));
209
+ }
210
+
211
+ SH_ALWAYS_INLINE ctrl_mask_t ctrl_match_empty_vec(ctrl_vec_t cv) {
212
+ return neon_mask64(vceqq_u8(cv, vdupq_n_u8((uint8_t)CTRL_EMPTY)));
213
+ }
214
+
215
+ SH_ALWAYS_INLINE ctrl_mask_t ctrl_match_empty_or_deleted_vec(ctrl_vec_t cv) {
216
+ return neon_mask64(vcltzq_s8(vreinterpretq_s8_u8(cv)));
217
+ }
218
+
219
+ SH_ALWAYS_INLINE ctrl_mask_t ctrl_match_empty(const uint8_t *ctrl) {
179
220
  return ctrl_match_empty_vec(ctrl_load(ctrl));
180
221
  }
181
222
 
@@ -216,6 +257,14 @@ SH_ALWAYS_INLINE uint32_t ctrl_match_empty(const uint8_t *ctrl) {
216
257
 
217
258
  #endif /* control-byte matching selection */
218
259
 
260
+ #ifndef CTRL_MASK_SLOT
261
+ typedef uint32_t ctrl_mask_t;
262
+ #define CTRL_MASK_SLOT(m) ctz32(m)
263
+ #endif
264
+
265
+ #if defined(__GNUC__) || defined(__clang__)
266
+ __attribute__((unused))
267
+ #endif
219
268
  SH_ALWAYS_INLINE int ctz32(uint32_t v) {
220
269
  #if defined(__GNUC__) || defined(__clang__)
221
270
  return __builtin_ctz(v);
@@ -233,6 +282,21 @@ SH_ALWAYS_INLINE int ctz32(uint32_t v) {
233
282
  #endif
234
283
  }
235
284
 
285
+ SH_ALWAYS_INLINE int ctz64(uint64_t v) {
286
+ #if defined(__GNUC__) || defined(__clang__)
287
+ return (int)__builtin_ctzll(v);
288
+ #elif defined(_MSC_VER)
289
+ unsigned long idx;
290
+ _BitScanForward64(&idx, v);
291
+ return (int)idx;
292
+ #else
293
+ if ((uint32_t)v) {
294
+ return ctz32((uint32_t)v);
295
+ }
296
+ return 32 + ctz32((uint32_t)(v >> 32));
297
+ #endif
298
+ }
299
+
236
300
  typedef struct {
237
301
  VALUE key;
238
302
  VALUE value;
@@ -250,44 +314,49 @@ typedef struct {
250
314
  uint8_t mutating;
251
315
  } SwissHash;
252
316
 
253
- #define MUTATE_GUARD_BEGIN(sh) \
254
- do { \
255
- if ((sh)->mutating) { \
256
- rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected " \
257
- "(#hash or #eql? callback modified the same table)"); \
258
- } \
259
- (sh)->mutating = 1; \
260
- } while (0)
261
-
262
- #define MUTATE_GUARD_END(sh) \
263
- do { \
264
- (sh)->mutating = 0; \
265
- } while (0)
266
-
267
317
  #define FIBONACCI_HASH_C 0x9E3779B97F4A7C15ULL
268
318
 
269
- SH_ALWAYS_INLINE uint64_t compute_hash(VALUE key) {
270
- uint64_t v;
319
+ SH_ALWAYS_INLINE uint64_t hash_fixnum(VALUE key) {
320
+ return ((uint64_t)FIX2LONG(key) ^ swiss_hash_seed0) * FIBONACCI_HASH_C;
321
+ }
271
322
 
323
+ SH_ALWAYS_INLINE uint64_t hash_immediate(VALUE key) {
272
324
  if (FIXNUM_P(key)) {
273
- v = (uint64_t)FIX2LONG(key) ^ swiss_hash_seed0;
274
- return v * FIBONACCI_HASH_C;
325
+ return hash_fixnum(key);
275
326
  }
327
+ return ((uint64_t)SYM2ID(key) ^ swiss_hash_seed0) * FIBONACCI_HASH_C;
328
+ }
276
329
 
277
- if (SYMBOL_P(key)) {
278
- v = (uint64_t)SYM2ID(key) ^ swiss_hash_seed0;
279
- return v * FIBONACCI_HASH_C;
330
+ SH_ALWAYS_INLINE uint64_t hash_string(VALUE key) {
331
+ const char *ptr = RSTRING_PTR(key);
332
+ long len = RSTRING_LEN(key);
333
+ uint64_t seed = swiss_hash_seed0;
334
+
335
+ if (len > 0) {
336
+ int enc = ENCODING_GET(key);
337
+ if (enc) {
338
+ int cr = ENC_CODERANGE(key);
339
+ if (cr == ENC_CODERANGE_UNKNOWN) {
340
+ cr = rb_enc_str_coderange(key);
341
+ }
342
+ if (cr != ENC_CODERANGE_7BIT) {
343
+ seed ^= (uint64_t)enc;
344
+ }
345
+ }
346
+ }
347
+ return wyhash(ptr ? ptr : (const char *)"", (size_t)len, seed);
348
+ }
349
+
350
+ SH_ALWAYS_INLINE uint64_t compute_hash(VALUE key) {
351
+ if (FIXNUM_P(key) || SYMBOL_P(key)) {
352
+ return hash_immediate(key);
280
353
  }
281
354
 
282
355
  if (RB_TYPE_P(key, T_STRING)) {
283
- const char *ptr = RSTRING_PTR(key);
284
- long len = RSTRING_LEN(key);
285
- int enc_idx = ENCODING_GET(key);
286
- uint64_t str_seed = swiss_hash_seed0 ^ (uint64_t)enc_idx;
287
- return wyhash(ptr ? ptr : (const char *)"", (size_t)len, str_seed);
356
+ return hash_string(key);
288
357
  }
289
358
 
290
- v = (uint64_t)NUM2LONG(rb_hash(key));
359
+ uint64_t v = (uint64_t)NUM2LONG(rb_hash(key));
291
360
  v ^= swiss_hash_seed1;
292
361
  v ^= v >> 33;
293
362
  v *= 0xff51afd7ed558ccdULL;
@@ -300,6 +369,32 @@ SH_ALWAYS_INLINE uint64_t compute_hash(VALUE key) {
300
369
  #define H1(hash) ((hash) >> 7)
301
370
  #define H2(hash) ((uint8_t)((hash) & H2_MASK))
302
371
 
372
+ SH_ALWAYS_INLINE int strings_equal(VALUE a, VALUE b) {
373
+ long la = RSTRING_LEN(a);
374
+ if (la != RSTRING_LEN(b))
375
+ return 0;
376
+
377
+ const char *pa = RSTRING_PTR(a);
378
+ const char *pb = RSTRING_PTR(b);
379
+ if (pa == pb)
380
+ return 1;
381
+
382
+ int ea = ENCODING_GET(a);
383
+ int eb = ENCODING_GET(b);
384
+ if (ea == eb) {
385
+ return memcmp(pa, pb, (size_t)la) == 0;
386
+ }
387
+
388
+ if (ENC_CODERANGE(a) == ENC_CODERANGE_7BIT && ENC_CODERANGE(b) == ENC_CODERANGE_7BIT) {
389
+ return memcmp(pa, pb, (size_t)la) == 0;
390
+ }
391
+
392
+ if (rb_enc_compatible(a, b)) {
393
+ return memcmp(pa, pb, (size_t)la) == 0;
394
+ }
395
+ return rb_eql(a, b);
396
+ }
397
+
303
398
  SH_ALWAYS_INLINE int keys_equal(VALUE a, VALUE b) {
304
399
  if (a == b)
305
400
  return 1;
@@ -307,28 +402,7 @@ SH_ALWAYS_INLINE int keys_equal(VALUE a, VALUE b) {
307
402
  return 0;
308
403
 
309
404
  if (RB_TYPE_P(a, T_STRING) && RB_TYPE_P(b, T_STRING)) {
310
- long la = RSTRING_LEN(a);
311
- if (la != RSTRING_LEN(b))
312
- return 0;
313
- const char *pa = RSTRING_PTR(a);
314
- const char *pb = RSTRING_PTR(b);
315
- if (pa == pb)
316
- return 1;
317
-
318
- int ea = ENCODING_GET(a);
319
- int eb = ENCODING_GET(b);
320
- if (ea == eb) {
321
- return memcmp(pa, pb, (size_t)la) == 0;
322
- }
323
-
324
- if (ENC_CODERANGE(a) == ENC_CODERANGE_7BIT && ENC_CODERANGE(b) == ENC_CODERANGE_7BIT) {
325
- return memcmp(pa, pb, (size_t)la) == 0;
326
- }
327
-
328
- if (rb_enc_compatible(a, b)) {
329
- return memcmp(pa, pb, (size_t)la) == 0;
330
- }
331
- return rb_eql(a, b);
405
+ return strings_equal(a, b);
332
406
  }
333
407
 
334
408
  return rb_eql(a, b);
@@ -345,28 +419,64 @@ SH_ALWAYS_INLINE VALUE prepare_key(VALUE key) {
345
419
  key = rb_str_new_frozen(key);
346
420
  }
347
421
  #endif
348
- rb_enc_str_coderange(key);
349
422
  }
350
423
  return key;
351
424
  }
352
425
 
426
+ SH_ALWAYS_INLINE size_t swiss_ctrl_bytes(size_t capacity) {
427
+ return (capacity + 15u) & ~(size_t)15u;
428
+ }
429
+
430
+ static int swiss_alloc_block(size_t capacity, uint8_t **ctrl, Slot **slots) {
431
+ if (capacity == 0 || capacity > (SIZE_MAX / (sizeof(Slot) + 16))) {
432
+ return 0;
433
+ }
434
+ size_t ctrl_bytes = swiss_ctrl_bytes(capacity);
435
+ if (capacity > (SIZE_MAX - ctrl_bytes) / sizeof(Slot)) {
436
+ return 0;
437
+ }
438
+ uint8_t *block = (uint8_t *)malloc(ctrl_bytes + capacity * sizeof(Slot));
439
+ if (!block) {
440
+ return 0;
441
+ }
442
+ *ctrl = block;
443
+ *slots = (Slot *)(block + ctrl_bytes);
444
+ return 1;
445
+ }
446
+
353
447
  static void swiss_free_arrays(SwissHash *sh) {
354
448
  free(sh->ctrl);
355
449
  sh->ctrl = NULL;
356
- free(sh->slots);
357
450
  sh->slots = NULL;
358
451
  }
359
452
 
360
453
  static void swiss_init(SwissHash *sh, size_t min_capacity) {
361
- size_t min_groups = (min_capacity + GROUP_SIZE - 1) / GROUP_SIZE;
454
+ size_t min_groups = min_capacity / GROUP_SIZE + (min_capacity % GROUP_SIZE != 0);
362
455
  size_t num_groups = 1;
363
- while (num_groups < min_groups)
456
+ while (num_groups < min_groups) {
457
+ if (num_groups > SIZE_MAX / 2) {
458
+ rb_raise(rb_eArgError, "SwissHash capacity is too large");
459
+ }
364
460
  num_groups <<= 1;
461
+ }
365
462
  if (num_groups < 2)
366
463
  num_groups = 2;
367
464
 
465
+ if (num_groups > SIZE_MAX / GROUP_SIZE) {
466
+ rb_raise(rb_eArgError, "SwissHash capacity is too large");
467
+ }
368
468
  size_t capacity = num_groups * GROUP_SIZE;
469
+ uint8_t *ctrl = NULL;
470
+ Slot *slots = NULL;
369
471
 
472
+ if (!swiss_alloc_block(capacity, &ctrl, &slots)) {
473
+ rb_raise(rb_eNoMemError, "failed to allocate SwissHash");
474
+ }
475
+
476
+ memset(ctrl, CTRL_EMPTY, capacity);
477
+ uint8_t *old_ctrl = sh->ctrl;
478
+ sh->ctrl = ctrl;
479
+ sh->slots = slots;
370
480
  sh->num_groups = num_groups;
371
481
  sh->group_mask = num_groups - 1;
372
482
  sh->capacity = capacity;
@@ -374,19 +484,7 @@ static void swiss_init(SwissHash *sh, size_t min_capacity) {
374
484
  sh->tombstone_count = 0;
375
485
  sh->mutating = 0;
376
486
  sh->growth_left = capacity * MAX_LOAD_NUM / MAX_LOAD_DEN;
377
-
378
- sh->ctrl = (uint8_t *)malloc(capacity);
379
- sh->slots = (Slot *)malloc(capacity * sizeof(Slot));
380
-
381
- if (!sh->ctrl || !sh->slots) {
382
- free(sh->ctrl);
383
- free(sh->slots);
384
- sh->ctrl = NULL;
385
- sh->slots = NULL;
386
- rb_raise(rb_eNoMemError, "failed to allocate SwissHash");
387
- }
388
-
389
- memset(sh->ctrl, CTRL_EMPTY, capacity);
487
+ free(old_ctrl);
390
488
  }
391
489
 
392
490
  typedef struct {
@@ -410,28 +508,75 @@ SH_ALWAYS_INLINE void probe_next(ProbeSeq *ps) {
410
508
 
411
509
  #define GROUP_OFF(gi) ((gi) * GROUP_SIZE)
412
510
 
413
- static VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
414
- uint64_t hash = compute_hash(key);
511
+ #ifdef SWISS_USE_VEC
512
+ #define SH_SCAN_GROUP(sh, off, h2, match, empty) \
513
+ do { \
514
+ ctrl_vec_t cv = ctrl_load((sh)->ctrl + (off)); \
515
+ SH_PREFETCH(&(sh)->slots[off]); \
516
+ (match) = ctrl_match_h2_vec(cv, (h2)); \
517
+ (empty) = ctrl_match_empty_vec(cv); \
518
+ } while (0)
519
+ #define SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail) \
520
+ do { \
521
+ ctrl_vec_t cv = ctrl_load((sh)->ctrl + (off)); \
522
+ SH_PREFETCH(&(sh)->slots[off]); \
523
+ (match) = ctrl_match_h2_vec(cv, (h2)); \
524
+ (empty) = ctrl_match_empty_vec(cv); \
525
+ (avail) = ctrl_match_empty_or_deleted_vec(cv); \
526
+ } while (0)
527
+ #else
528
+ #define SH_SCAN_GROUP(sh, off, h2, match, empty) \
529
+ do { \
530
+ SH_PREFETCH(&(sh)->slots[off]); \
531
+ (match) = ctrl_match_h2_raw((sh)->ctrl + (off), (h2)); \
532
+ (empty) = ctrl_match_empty_raw((sh)->ctrl + (off)); \
533
+ } while (0)
534
+ #define SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail) \
535
+ do { \
536
+ SH_PREFETCH(&(sh)->slots[off]); \
537
+ (match) = ctrl_match_h2_raw((sh)->ctrl + (off), (h2)); \
538
+ (empty) = ctrl_match_empty_raw((sh)->ctrl + (off)); \
539
+ (avail) = ctrl_match_empty_or_deleted_raw((sh)->ctrl + (off)); \
540
+ } while (0)
541
+ #endif
542
+
543
+ static inline VALUE *swiss_lookup_hashed(SwissHash *sh, VALUE key, uint64_t hash, int identity_only) {
415
544
  uint8_t h2 = H2(hash);
416
545
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
417
546
 
418
547
  for (;;) {
419
548
  size_t off = GROUP_OFF(ps.group_idx);
549
+ ctrl_mask_t match, empty;
550
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
551
+
552
+ while (match) {
553
+ int slot = CTRL_MASK_SLOT(match);
554
+ Slot *s = &sh->slots[off + slot];
555
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
556
+ return &s->value;
557
+ }
558
+ match &= match - 1;
559
+ }
560
+
561
+ if (empty)
562
+ return NULL;
563
+ probe_next(&ps);
564
+ }
565
+ }
566
+
567
+ static inline VALUE *swiss_lookup_string(SwissHash *sh, VALUE key, uint64_t hash) {
568
+ uint8_t h2 = H2(hash);
569
+ ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
570
+
571
+ for (;;) {
572
+ size_t off = GROUP_OFF(ps.group_idx);
573
+ ctrl_mask_t match, empty;
574
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
420
575
 
421
- #if defined(SWISS_USE_SSE2)
422
- __m128i cv = ctrl_load(sh->ctrl + off);
423
- SH_PREFETCH(&sh->slots[off]);
424
- uint32_t match = ctrl_match_h2_vec(cv, h2);
425
- uint32_t empty = ctrl_match_empty_vec(cv);
426
- #else
427
- SH_PREFETCH(&sh->slots[off]);
428
- uint32_t match = ctrl_match_h2_raw(sh->ctrl + off, h2);
429
- uint32_t empty = ctrl_match_empty_raw(sh->ctrl + off);
430
- #endif
431
576
  while (match) {
432
- int slot = ctz32(match);
577
+ int slot = CTRL_MASK_SLOT(match);
433
578
  Slot *s = &sh->slots[off + slot];
434
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
579
+ if (RB_LIKELY(s->key == key) || (RB_TYPE_P(s->key, T_STRING) && strings_equal(s->key, key))) {
435
580
  return &s->value;
436
581
  }
437
582
  match &= match - 1;
@@ -443,23 +588,75 @@ static VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
443
588
  }
444
589
  }
445
590
 
591
+ typedef struct {
592
+ SwissHash *sh;
593
+ VALUE key;
594
+ VALUE value;
595
+ VALUE result;
596
+ VALUE *found;
597
+ } CallbackCtx;
598
+
599
+ SH_ALWAYS_INLINE int key_may_callback(VALUE key) {
600
+ return !(FIXNUM_P(key) || SYMBOL_P(key) || RB_TYPE_P(key, T_STRING));
601
+ }
602
+
603
+ static void swiss_lock(SwissHash *sh) {
604
+ if (sh->mutating) {
605
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
606
+ "(#hash or #eql? callback modified the same table)");
607
+ }
608
+ sh->mutating = 1;
609
+ }
610
+
611
+ static VALUE swiss_unlock_ensure(VALUE shv) {
612
+ ((SwissHash *)shv)->mutating = 0;
613
+ return Qnil;
614
+ }
615
+
616
+ static VALUE swiss_lookup_custom(VALUE arg) {
617
+ CallbackCtx *c = (CallbackCtx *)arg;
618
+ c->found = swiss_lookup_hashed(c->sh, c->key, compute_hash(c->key), 0);
619
+ return Qnil;
620
+ }
621
+
622
+ static inline VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
623
+ if (FIXNUM_P(key)) {
624
+ return swiss_lookup_hashed(sh, key, hash_fixnum(key), 1);
625
+ }
626
+ if (SYMBOL_P(key)) {
627
+ return swiss_lookup_hashed(sh, key, hash_immediate(key), 1);
628
+ }
629
+ if (RB_TYPE_P(key, T_STRING)) {
630
+ return swiss_lookup_string(sh, key, hash_string(key));
631
+ }
632
+ if (sh->mutating) {
633
+ return swiss_lookup_hashed(sh, key, compute_hash(key), 0);
634
+ }
635
+ CallbackCtx ctx = {sh, key, Qnil, Qnil, NULL};
636
+ swiss_lock(sh);
637
+ rb_ensure(swiss_lookup_custom, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
638
+ return ctx.found;
639
+ }
640
+
446
641
  static void swiss_grow(SwissHash *sh);
447
642
  static void swiss_compact(SwissHash *sh);
448
643
 
449
- SH_ALWAYS_INLINE void swiss_insert_rehash(SwissHash *sh, uint64_t hash, VALUE key, VALUE value) {
644
+ SH_ALWAYS_INLINE void swiss_insert_rehash_into(uint8_t *ctrl, Slot *slots, size_t group_mask,
645
+ size_t *size, size_t *growth_left,
646
+ uint64_t hash, VALUE key, VALUE value) {
450
647
  uint8_t h2 = H2(hash);
451
- ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
648
+ ProbeSeq ps = probe_start(H1(hash), group_mask);
452
649
 
453
650
  for (;;) {
454
651
  size_t off = GROUP_OFF(ps.group_idx);
455
- uint32_t empty_mask = ctrl_match_empty(sh->ctrl + off);
652
+ ctrl_mask_t empty_mask = ctrl_match_empty(ctrl + off);
456
653
  if (empty_mask) {
457
- size_t idx = off + ctz32(empty_mask);
458
- sh->ctrl[idx] = h2;
459
- sh->slots[idx].key = key;
460
- sh->slots[idx].value = value;
461
- sh->size++;
462
- sh->growth_left--;
654
+ size_t idx = off + (size_t)CTRL_MASK_SLOT(empty_mask);
655
+ ctrl[idx] = h2;
656
+ slots[idx].key = key;
657
+ slots[idx].value = value;
658
+ (*size)++;
659
+ (*growth_left)--;
463
660
  return;
464
661
  }
465
662
  probe_next(&ps);
@@ -470,44 +667,87 @@ static inline int should_compact(SwissHash *sh) {
470
667
  return sh->tombstone_count >= sh->capacity / TOMBSTONE_COMPACT_DIVISOR;
471
668
  }
472
669
 
473
- static void swiss_compact(SwissHash *sh) {
474
- size_t cap = sh->capacity;
475
- size_t old_size = sh->size;
476
-
477
- uint8_t *old_ctrl = sh->ctrl;
478
- Slot *old_slots = sh->slots;
670
+ typedef struct {
671
+ SwissHash *sh;
672
+ uint8_t *new_ctrl;
673
+ Slot *new_slots;
674
+ const uint8_t *old_ctrl;
675
+ const Slot *old_slots;
676
+ size_t old_cap;
677
+ size_t new_cap;
678
+ size_t new_num_groups;
679
+ } RebuildCtx;
680
+
681
+ static VALUE swiss_rebuild_body(VALUE arg) {
682
+ RebuildCtx *ctx = (RebuildCtx *)arg;
683
+ size_t group_mask = ctx->new_num_groups - 1;
684
+ size_t size = 0;
685
+ size_t growth_left = ctx->new_cap * MAX_LOAD_NUM / MAX_LOAD_DEN;
686
+
687
+ for (size_t i = 0; i < ctx->old_cap; i++) {
688
+ uint8_t c = ctx->old_ctrl[i];
689
+ if (c != CTRL_EMPTY && c != CTRL_DELETED) {
690
+ VALUE key = ctx->old_slots[i].key;
691
+ uint64_t hash = FIXNUM_P(key) ? hash_fixnum(key) : compute_hash(key);
692
+ swiss_insert_rehash_into(ctx->new_ctrl, ctx->new_slots, group_mask,
693
+ &size, &growth_left, hash, key, ctx->old_slots[i].value);
694
+ }
695
+ }
479
696
 
480
- sh->ctrl = (uint8_t *)malloc(cap);
481
- sh->slots = (Slot *)malloc(cap * sizeof(Slot));
697
+ free(ctx->sh->ctrl);
698
+ ctx->sh->ctrl = ctx->new_ctrl;
699
+ ctx->sh->slots = ctx->new_slots;
700
+ ctx->sh->num_groups = ctx->new_num_groups;
701
+ ctx->sh->group_mask = group_mask;
702
+ ctx->sh->capacity = ctx->new_cap;
703
+ ctx->sh->size = size;
704
+ ctx->sh->growth_left = growth_left;
705
+ ctx->sh->tombstone_count = 0;
706
+ ctx->new_ctrl = NULL;
707
+ ctx->new_slots = NULL;
708
+ return Qnil;
709
+ }
482
710
 
483
- if (!sh->ctrl || !sh->slots) {
484
- free(sh->ctrl);
485
- free(sh->slots);
486
- sh->ctrl = old_ctrl;
487
- sh->slots = old_slots;
488
- sh->size = old_size;
489
- sh->growth_left = 0;
490
- rb_raise(rb_eNoMemError, "failed to compact SwissHash");
711
+ static VALUE swiss_rebuild_ensure(VALUE arg) {
712
+ RebuildCtx *ctx = (RebuildCtx *)arg;
713
+ if (ctx->new_ctrl) {
714
+ free(ctx->new_ctrl);
715
+ ctx->new_ctrl = NULL;
716
+ ctx->new_slots = NULL;
491
717
  }
718
+ return Qnil;
719
+ }
492
720
 
493
- memset(sh->ctrl, CTRL_EMPTY, cap);
494
- sh->size = 0;
495
- sh->growth_left = cap * MAX_LOAD_NUM / MAX_LOAD_DEN;
496
- sh->tombstone_count = 0;
721
+ static void swiss_rebuild(SwissHash *sh, size_t new_num_groups) {
722
+ RebuildCtx ctx;
723
+ memset(&ctx, 0, sizeof(ctx));
724
+ ctx.sh = sh;
725
+ ctx.old_ctrl = sh->ctrl;
726
+ ctx.old_slots = sh->slots;
727
+ ctx.old_cap = sh->capacity;
728
+ ctx.new_num_groups = new_num_groups;
729
+ ctx.new_cap = new_num_groups * GROUP_SIZE;
497
730
 
498
- for (size_t i = 0; i < cap; i++) {
499
- uint8_t c = old_ctrl[i];
500
- if (c != CTRL_EMPTY && c != CTRL_DELETED) {
501
- uint64_t hash = compute_hash(old_slots[i].key);
502
- swiss_insert_rehash(sh, hash, old_slots[i].key, old_slots[i].value);
503
- }
731
+ if (!swiss_alloc_block(ctx.new_cap, &ctx.new_ctrl, &ctx.new_slots)) {
732
+ rb_raise(rb_eNoMemError, "failed to rebuild SwissHash");
504
733
  }
734
+ memset(ctx.new_ctrl, CTRL_EMPTY, ctx.new_cap);
735
+ rb_ensure(swiss_rebuild_body, (VALUE)&ctx, swiss_rebuild_ensure, (VALUE)&ctx);
736
+ }
505
737
 
506
- free(old_ctrl);
507
- free(old_slots);
738
+ SH_ALWAYS_INLINE void swiss_place_new(SwissHash *sh, size_t insert_idx, uint8_t h2, VALUE key, VALUE value) {
739
+ if (sh->ctrl[insert_idx] == CTRL_EMPTY) {
740
+ sh->growth_left--;
741
+ } else {
742
+ sh->tombstone_count--;
743
+ }
744
+ sh->ctrl[insert_idx] = h2;
745
+ sh->slots[insert_idx].key = key;
746
+ sh->slots[insert_idx].value = value;
747
+ sh->size++;
508
748
  }
509
749
 
510
- static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
750
+ static VALUE swiss_insert_hashed(SwissHash *sh, VALUE key, VALUE value, uint64_t hash, int identity_only, int may_callback) {
511
751
  if (sh->growth_left == 0) {
512
752
  if (should_compact(sh)) {
513
753
  swiss_compact(sh);
@@ -516,32 +756,33 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
516
756
  }
517
757
  }
518
758
 
519
- uint64_t hash = compute_hash(key);
520
759
  uint8_t h2 = H2(hash);
521
760
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
522
-
523
761
  size_t insert_idx = (size_t)-1;
524
762
 
525
763
  for (;;) {
526
764
  size_t off = GROUP_OFF(ps.group_idx);
527
-
528
- #if defined(SWISS_USE_SSE2)
529
- __m128i cv = ctrl_load(sh->ctrl + off);
530
- SH_PREFETCH(&sh->slots[off]);
531
- uint32_t match = ctrl_match_h2_vec(cv, h2);
532
- uint32_t empty = ctrl_match_empty_vec(cv);
533
- uint32_t avail = ctrl_match_empty_or_deleted_vec(cv);
765
+ ctrl_mask_t match, empty, avail;
766
+ #ifdef SWISS_USE_VEC
767
+ {
768
+ ctrl_vec_t cv = ctrl_load(sh->ctrl + off);
769
+ SH_PREFETCH(&sh->slots[off]);
770
+ match = ctrl_match_h2_vec(cv, h2);
771
+ empty = ctrl_match_empty_vec(cv);
772
+ avail = (sh->tombstone_count == 0) ? empty : ctrl_match_empty_or_deleted_vec(cv);
773
+ }
534
774
  #else
535
- SH_PREFETCH(&sh->slots[off]);
536
- uint32_t match = ctrl_match_h2_raw(sh->ctrl + off, h2);
537
- uint32_t empty = ctrl_match_empty_raw(sh->ctrl + off);
538
- uint32_t avail = ctrl_match_empty_or_deleted_raw(sh->ctrl + off);
775
+ SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail);
776
+ if (sh->tombstone_count == 0) {
777
+ avail = empty;
778
+ }
539
779
  #endif
780
+
540
781
  while (match) {
541
- int slot = ctz32(match);
782
+ int slot = CTRL_MASK_SLOT(match);
542
783
  size_t idx = off + slot;
543
784
  Slot *s = &sh->slots[idx];
544
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
785
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
545
786
  s->value = value;
546
787
  return value;
547
788
  }
@@ -549,7 +790,7 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
549
790
  }
550
791
 
551
792
  if (insert_idx == (size_t)-1 && avail) {
552
- insert_idx = off + ctz32(avail);
793
+ insert_idx = off + (size_t)CTRL_MASK_SLOT(avail);
553
794
  }
554
795
 
555
796
  if (empty)
@@ -557,108 +798,172 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
557
798
  probe_next(&ps);
558
799
  }
559
800
 
560
- MUTATE_GUARD_BEGIN(sh);
801
+ swiss_place_new(sh, insert_idx, h2, key, value);
802
+ return value;
803
+ }
561
804
 
562
- if (sh->ctrl[insert_idx] == CTRL_EMPTY) {
563
- sh->growth_left--;
564
- } else {
565
- sh->tombstone_count--;
805
+ SH_ALWAYS_INLINE VALUE swiss_insert_fixnum(SwissHash *sh, VALUE key, VALUE value) {
806
+ if (RB_UNLIKELY(sh->growth_left == 0)) {
807
+ if (should_compact(sh)) {
808
+ swiss_compact(sh);
809
+ } else {
810
+ swiss_grow(sh);
811
+ }
566
812
  }
567
- sh->ctrl[insert_idx] = h2;
568
- sh->slots[insert_idx].key = key;
569
- sh->slots[insert_idx].value = value;
570
- sh->size++;
571
813
 
572
- MUTATE_GUARD_END(sh);
814
+ uint64_t hash = hash_fixnum(key);
815
+ uint8_t h2 = H2(hash);
816
+ ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
817
+ size_t insert_idx = (size_t)-1;
818
+
819
+ for (;;) {
820
+ size_t off = GROUP_OFF(ps.group_idx);
821
+ ctrl_mask_t match, empty, avail;
822
+ #ifdef SWISS_USE_VEC
823
+ ctrl_vec_t cv = ctrl_load(sh->ctrl + off);
824
+ SH_PREFETCH(&sh->slots[off]);
825
+ match = ctrl_match_h2_vec(cv, h2);
826
+ empty = ctrl_match_empty_vec(cv);
827
+ avail = (sh->tombstone_count == 0) ? empty : ctrl_match_empty_or_deleted_vec(cv);
828
+ #else
829
+ SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail);
830
+ if (sh->tombstone_count == 0) {
831
+ avail = empty;
832
+ }
833
+ #endif
834
+ while (match) {
835
+ int slot = CTRL_MASK_SLOT(match);
836
+ if (RB_LIKELY(sh->slots[off + slot].key == key)) {
837
+ sh->slots[off + slot].value = value;
838
+ return value;
839
+ }
840
+ match &= match - 1;
841
+ }
842
+
843
+ if (insert_idx == (size_t)-1 && avail) {
844
+ insert_idx = off + (size_t)CTRL_MASK_SLOT(avail);
845
+ }
846
+ if (RB_LIKELY(empty))
847
+ break;
848
+ probe_next(&ps);
849
+ }
850
+
851
+ swiss_place_new(sh, insert_idx, h2, key, value);
573
852
  return value;
574
853
  }
575
854
 
576
- static VALUE swiss_delete(SwissHash *sh, VALUE key) {
855
+ static VALUE swiss_insert_unlocked(VALUE arg) {
856
+ CallbackCtx *c = (CallbackCtx *)arg;
857
+ if (FIXNUM_P(c->key)) {
858
+ c->result = swiss_insert_fixnum(c->sh, c->key, c->value);
859
+ } else if (SYMBOL_P(c->key)) {
860
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, hash_immediate(c->key), 1, 0);
861
+ } else if (RB_TYPE_P(c->key, T_STRING)) {
862
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, hash_string(c->key), 0, 0);
863
+ } else {
864
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, compute_hash(c->key), 0, 0);
865
+ }
866
+ return c->result;
867
+ }
868
+
869
+ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
870
+ if (sh->mutating) {
871
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
872
+ "(#hash or #eql? callback modified the same table)");
873
+ }
874
+
875
+ CallbackCtx ctx = {sh, key, value, Qnil, NULL};
876
+ if (!key_may_callback(key) && sh->growth_left != 0) {
877
+ return swiss_insert_unlocked((VALUE)&ctx);
878
+ }
879
+
880
+ swiss_lock(sh);
881
+ rb_ensure(swiss_insert_unlocked, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
882
+ return ctx.result;
883
+ }
884
+
885
+ static VALUE swiss_delete_unlocked(VALUE arg) {
886
+ CallbackCtx *c = (CallbackCtx *)arg;
887
+ SwissHash *sh = c->sh;
888
+ VALUE key = c->key;
577
889
  uint64_t hash = compute_hash(key);
890
+ int identity_only = FIXNUM_P(key) || SYMBOL_P(key);
578
891
  uint8_t h2 = H2(hash);
579
892
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
580
893
 
581
894
  for (;;) {
582
895
  size_t off = GROUP_OFF(ps.group_idx);
896
+ ctrl_mask_t match, empty;
897
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
583
898
 
584
- #if defined(SWISS_USE_SSE2)
585
- __m128i cv = ctrl_load(sh->ctrl + off);
586
- SH_PREFETCH(&sh->slots[off]);
587
- uint32_t match = ctrl_match_h2_vec(cv, h2);
588
- uint32_t empty = ctrl_match_empty_vec(cv);
589
- #else
590
- SH_PREFETCH(&sh->slots[off]);
591
- uint32_t match = ctrl_match_h2_raw(sh->ctrl + off, h2);
592
- uint32_t empty = ctrl_match_empty_raw(sh->ctrl + off);
593
- #endif
594
899
  while (match) {
595
- int slot = ctz32(match);
900
+ int slot = CTRL_MASK_SLOT(match);
596
901
  size_t idx = off + slot;
597
902
  Slot *s = &sh->slots[idx];
598
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
903
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
599
904
  VALUE old_value = s->value;
600
-
601
- MUTATE_GUARD_BEGIN(sh);
602
905
  sh->ctrl[idx] = CTRL_DELETED;
603
906
  sh->slots[idx].key = Qnil;
604
907
  sh->slots[idx].value = Qnil;
605
908
  sh->size--;
606
909
  sh->tombstone_count++;
607
- MUTATE_GUARD_END(sh);
608
-
910
+ c->result = old_value;
609
911
  return old_value;
610
912
  }
611
913
  match &= match - 1;
612
914
  }
613
915
 
614
- if (empty)
916
+ if (empty) {
917
+ c->result = Qnil;
615
918
  return Qnil;
919
+ }
616
920
  probe_next(&ps);
617
921
  }
618
922
  }
619
923
 
620
- static void swiss_grow(SwissHash *sh) {
621
- size_t old_cap = sh->capacity;
622
- size_t old_size = sh->size;
623
-
624
- uint8_t *old_ctrl = sh->ctrl;
625
- Slot *old_slots = sh->slots;
626
-
627
- size_t new_num_groups = sh->num_groups * 2;
628
- size_t new_cap = new_num_groups * GROUP_SIZE;
629
-
630
- sh->ctrl = (uint8_t *)malloc(new_cap);
631
- sh->slots = (Slot *)malloc(new_cap * sizeof(Slot));
924
+ static VALUE swiss_delete(SwissHash *sh, VALUE key) {
925
+ if (sh->mutating) {
926
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
927
+ "(#hash or #eql? callback modified the same table)");
928
+ }
632
929
 
633
- if (!sh->ctrl || !sh->slots) {
634
- free(sh->ctrl);
635
- free(sh->slots);
636
- sh->ctrl = old_ctrl;
637
- sh->slots = old_slots;
638
- sh->size = old_size;
639
- sh->growth_left = 0;
640
- rb_raise(rb_eNoMemError, "failed to grow SwissHash");
930
+ CallbackCtx ctx = {sh, key, Qnil, Qnil, NULL};
931
+ if (!key_may_callback(key)) {
932
+ return swiss_delete_unlocked((VALUE)&ctx);
641
933
  }
642
934
 
643
- memset(sh->ctrl, CTRL_EMPTY, new_cap);
935
+ swiss_lock(sh);
936
+ rb_ensure(swiss_delete_unlocked, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
937
+ return ctx.result;
938
+ }
644
939
 
645
- sh->num_groups = new_num_groups;
646
- sh->group_mask = new_num_groups - 1;
647
- sh->capacity = new_cap;
648
- sh->size = 0;
649
- sh->growth_left = new_cap * MAX_LOAD_NUM / MAX_LOAD_DEN;
650
- sh->tombstone_count = 0;
940
+ typedef struct {
941
+ SwissHash *sh;
942
+ size_t new_num_groups;
943
+ } RebuildCall;
651
944
 
652
- for (size_t i = 0; i < old_cap; i++) {
653
- uint8_t c = old_ctrl[i];
654
- if (c != CTRL_EMPTY && c != CTRL_DELETED) {
655
- uint64_t hash = compute_hash(old_slots[i].key);
656
- swiss_insert_rehash(sh, hash, old_slots[i].key, old_slots[i].value);
657
- }
945
+ static VALUE swiss_rebuild_call(VALUE arg) {
946
+ RebuildCall *c = (RebuildCall *)arg;
947
+ swiss_rebuild(c->sh, c->new_num_groups);
948
+ return Qnil;
949
+ }
950
+
951
+ static void swiss_rebuild_locked(SwissHash *sh, size_t new_num_groups) {
952
+ RebuildCall call = {sh, new_num_groups};
953
+ if (sh->mutating) {
954
+ swiss_rebuild(sh, new_num_groups);
955
+ return;
658
956
  }
957
+ swiss_lock(sh);
958
+ rb_ensure(swiss_rebuild_call, (VALUE)&call, swiss_unlock_ensure, (VALUE)sh);
959
+ }
659
960
 
660
- free(old_ctrl);
661
- free(old_slots);
961
+ static void swiss_grow(SwissHash *sh) {
962
+ swiss_rebuild_locked(sh, sh->num_groups * 2);
963
+ }
964
+
965
+ static void swiss_compact(SwissHash *sh) {
966
+ swiss_rebuild_locked(sh, sh->num_groups);
662
967
  }
663
968
 
664
969
  static void swiss_hash_mark(void *ptr) {
@@ -712,6 +1017,10 @@ static VALUE swiss_hash_initialize(int argc, VALUE *argv, VALUE self) {
712
1017
  SwissHash *sh;
713
1018
  TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, sh);
714
1019
 
1020
+ if (sh->mutating) {
1021
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected");
1022
+ }
1023
+
715
1024
  size_t capacity = NIL_P(capacity_val) ? 16 : NUM2SIZET(capacity_val);
716
1025
  swiss_init(sh, capacity);
717
1026
 
@@ -728,10 +1037,6 @@ static VALUE swiss_hash_initialize_copy(VALUE self, VALUE original) {
728
1037
  TypedData_Get_Struct(original, SwissHash, &swiss_hash_type, src);
729
1038
  TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, dst);
730
1039
 
731
- if (dst->ctrl || dst->slots) {
732
- swiss_free_arrays(dst);
733
- }
734
-
735
1040
  if (!src->ctrl || !src->slots) {
736
1041
  swiss_init(dst, 16);
737
1042
  return self;
@@ -750,7 +1055,14 @@ static VALUE swiss_hash_initialize_copy(VALUE self, VALUE original) {
750
1055
 
751
1056
  static VALUE swiss_hash_aset(VALUE self, VALUE key, VALUE value) {
752
1057
  SwissHash *sh = (SwissHash *)RTYPEDDATA_DATA(self);
753
- if (RB_UNLIKELY(!(FIXNUM_P(key) || SYMBOL_P(key)))) {
1058
+ if (RB_UNLIKELY(sh->mutating)) {
1059
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
1060
+ "(#hash or #eql? callback modified the same table)");
1061
+ }
1062
+ if (FIXNUM_P(key)) {
1063
+ return swiss_insert_fixnum(sh, key, value);
1064
+ }
1065
+ if (RB_UNLIKELY(!SYMBOL_P(key))) {
754
1066
  key = prepare_key(key);
755
1067
  }
756
1068
  return swiss_insert(sh, key, value);
@@ -758,7 +1070,12 @@ static VALUE swiss_hash_aset(VALUE self, VALUE key, VALUE value) {
758
1070
 
759
1071
  static VALUE swiss_hash_aref(VALUE self, VALUE key) {
760
1072
  SwissHash *sh = (SwissHash *)RTYPEDDATA_DATA(self);
761
- VALUE *val = swiss_lookup(sh, key);
1073
+ VALUE *val;
1074
+ if (FIXNUM_P(key)) {
1075
+ val = swiss_lookup_hashed(sh, key, hash_fixnum(key), 1);
1076
+ } else {
1077
+ val = swiss_lookup(sh, key);
1078
+ }
762
1079
  return val ? *val : Qnil;
763
1080
  }
764
1081
 
@@ -783,6 +1100,11 @@ static VALUE swiss_hash_clear(VALUE self) {
783
1100
  SwissHash *sh;
784
1101
  TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, sh);
785
1102
 
1103
+ if (RB_UNLIKELY(sh->mutating)) {
1104
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
1105
+ "(#hash or #eql? callback modified the same table)");
1106
+ }
1107
+
786
1108
  memset(sh->ctrl, CTRL_EMPTY, sh->capacity);
787
1109
  memset(sh->slots, 0, sh->capacity * sizeof(Slot));
788
1110
  sh->size = 0;
@@ -1057,8 +1379,16 @@ static VALUE swiss_hash_merge(int argc, VALUE *argv, VALUE self) {
1057
1379
  }
1058
1380
 
1059
1381
  static VALUE swiss_hash_replace(VALUE self, VALUE other) {
1382
+ if (self == other) {
1383
+ return self;
1384
+ }
1385
+
1386
+ VALUE converted = rb_check_hash_type(other);
1387
+ if (NIL_P(converted) && !RTEST(rb_obj_is_kind_of(other, cSwissHashHash))) {
1388
+ rb_raise(rb_eTypeError, "no implicit conversion of %s into Hash", rb_obj_classname(other));
1389
+ }
1060
1390
  swiss_hash_clear(self);
1061
- VALUE argv[1] = {other};
1391
+ VALUE argv[1] = {NIL_P(converted) ? other : converted};
1062
1392
  swiss_hash_merge_bang(1, argv, self);
1063
1393
  return self;
1064
1394
  }
@@ -1401,6 +1731,8 @@ static VALUE swiss_hash_stats(VALUE self) {
1401
1731
 
1402
1732
  #ifdef SWISS_USE_SSE2
1403
1733
  rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("SSE2"));
1734
+ #elif defined(SWISS_USE_NEON)
1735
+ rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("NEON"));
1404
1736
  #else
1405
1737
  rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("SWAR"));
1406
1738
  #endif
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwissHash
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swiss_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haidarov