swiss_hash 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd83d549a02b9134baf31b4fa97b7eca44f370653cb1a3af0076f9d1196c1e15
4
- data.tar.gz: 11a81f20822f58be2f18936e9a7e5bda112759868d91a8d7e2dcc1332bce598a
3
+ metadata.gz: b9d4f872535c0a7e7558f6c7950a2fb113263ab9b75cd2397bff72ee4185c295
4
+ data.tar.gz: ad110fb4e54d662181d3e49546eec833a953be7b87debae477837ad8e50408a5
5
5
  SHA512:
6
- metadata.gz: 79009afb0d1ef895a975ac2ad10c612845f5e81e473e4476e9cb7a5732f482241a5ebb73427633aeecffaf3c516406e8f88c99c69d02da5db19c8ca1aa8db53d
7
- data.tar.gz: 0736cadfdf1e688e4c47f29c120edc8c9eddc1602b9ab933fd740fd954db7af076ae1eea4f333028d2be8edf9e066ab7797a4dc1ec6faf91b7fd37a7f10f58bc
6
+ metadata.gz: 85cecddcb36ff970d62bd053bb78c7b42f0f15290276f64326ecc053236ca6ff473ed016933dd848ce9d9f4e5b63efdebb014140a84b22436e9bb9614567b4cf
7
+ data.tar.gz: 2cf36fed40554508684932d155bad203b4da18d9b808d2a8317b5f0e46b5a2e3dee9ebe3ab963ecdbd75406e99075d816d2dcf9b1277b562c6b421a845b1d8ec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
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
+
3
12
  ## 0.1.2 - Unreleased
4
13
 
5
14
  Documentation and API polish for the first public release candidate.
data/README.md CHANGED
@@ -27,52 +27,52 @@ h.stats # => { capacity: 16, size: 0, ... }
27
27
 
28
28
  Benchmarks below were produced by `benchmark.rb` on Ruby 3.4.3 / arm64-darwin24.
29
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.
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.
31
31
 
32
32
  ### N = 100,000
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) | 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%** ⚡ |
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.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%** ⚡ |
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.
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.
59
59
 
60
60
  | Operation | Ruby Hash | SwissHash | Delta |
61
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% |
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%** |
69
69
 
70
70
  ### Summary
71
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.
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
76
  - Ruby's built-in `Hash` remains excellent, especially for very small maps and cases that benefit from VM-level Hash specialization.
77
77
 
78
78
  ### Memory Usage
@@ -84,12 +84,13 @@ For 100,000 integer keys in the current benchmark:
84
84
  | SwissHash | 2,176 KB native + 4 GC slots |
85
85
  | Ruby Hash | 3 GC slots; native memory not directly measurable from this benchmark |
86
86
 
87
- Additional stats: load factor 76.3%, max load factor 87.5%, SIMD path reported as SWAR on the benchmark machine.
87
+ Additional stats: load factor 76.3%, max load factor 87.5%, SIMD path reported as NEON on the benchmarked Apple Silicon machine.
88
88
 
89
89
  ## Features
90
90
 
91
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.
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.
93
94
  - **Low GC pressure**: keys and values are Ruby objects, while control bytes and slots live in contiguous native arrays.
94
95
  - **Delete/reinsert friendly**: tombstones are tracked and compacted to avoid pathological slowdown.
95
96
  - **Hash-like API**: basic accessors, enumeration, fetch helpers, merge/update/replace, filtering, transforming, slicing, inversion, and conversion helpers.
@@ -200,7 +201,7 @@ Stick with Ruby's built-in `Hash` when:
200
201
  ### Swiss Table core
201
202
 
202
203
  - **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
+ - **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
205
  - **Triangular probing** — `i(i+1)/2` — over power-of-two group counts.
205
206
  - **Max load factor 87.5%** (7/8).
206
207
 
@@ -215,7 +216,7 @@ Stick with Ruby's built-in `Hash` when:
215
216
 
216
217
  ### Memory layout
217
218
 
218
- - Separate control-byte array and slot array.
219
+ - One native allocation for control bytes followed by slots (16-byte aligned). `stats[:memory_bytes]` still counts `capacity * (1 + sizeof(Slot))`.
219
220
  - Native arrays are allocated outside Ruby's object heap; keys and values are still marked for GC.
220
221
  - Slot memory is not zero-initialized on allocation; slots are read only after their control byte marks them live.
221
222
 
@@ -231,6 +232,7 @@ bundle exec rake compile
231
232
  ```bash
232
233
  bundle exec ruby test/hash_api_test.rb
233
234
  bundle exec ruby test/string_key_mutation_test.rb
235
+ bundle exec ruby test/safety_and_encoding_test.rb
234
236
  ```
235
237
 
236
238
  ## 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,19 +419,42 @@ 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) {
454
+ if (sh->ctrl) {
455
+ swiss_free_arrays(sh);
456
+ }
457
+
361
458
  size_t min_groups = (min_capacity + GROUP_SIZE - 1) / GROUP_SIZE;
362
459
  size_t num_groups = 1;
363
460
  while (num_groups < min_groups)
@@ -366,7 +463,22 @@ static void swiss_init(SwissHash *sh, size_t min_capacity) {
366
463
  num_groups = 2;
367
464
 
368
465
  size_t capacity = num_groups * GROUP_SIZE;
466
+ uint8_t *ctrl = NULL;
467
+ Slot *slots = NULL;
468
+
469
+ if (!swiss_alloc_block(capacity, &ctrl, &slots)) {
470
+ sh->capacity = 0;
471
+ sh->num_groups = 0;
472
+ sh->group_mask = 0;
473
+ sh->size = 0;
474
+ sh->growth_left = 0;
475
+ sh->tombstone_count = 0;
476
+ sh->mutating = 0;
477
+ rb_raise(rb_eNoMemError, "failed to allocate SwissHash");
478
+ }
369
479
 
480
+ sh->ctrl = ctrl;
481
+ sh->slots = slots;
370
482
  sh->num_groups = num_groups;
371
483
  sh->group_mask = num_groups - 1;
372
484
  sh->capacity = capacity;
@@ -374,18 +486,6 @@ static void swiss_init(SwissHash *sh, size_t min_capacity) {
374
486
  sh->tombstone_count = 0;
375
487
  sh->mutating = 0;
376
488
  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
489
  memset(sh->ctrl, CTRL_EMPTY, capacity);
390
490
  }
391
491
 
@@ -410,28 +510,51 @@ SH_ALWAYS_INLINE void probe_next(ProbeSeq *ps) {
410
510
 
411
511
  #define GROUP_OFF(gi) ((gi) * GROUP_SIZE)
412
512
 
413
- static VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
414
- uint64_t hash = compute_hash(key);
513
+ #ifdef SWISS_USE_VEC
514
+ #define SH_SCAN_GROUP(sh, off, h2, match, empty) \
515
+ do { \
516
+ ctrl_vec_t cv = ctrl_load((sh)->ctrl + (off)); \
517
+ SH_PREFETCH(&(sh)->slots[off]); \
518
+ (match) = ctrl_match_h2_vec(cv, (h2)); \
519
+ (empty) = ctrl_match_empty_vec(cv); \
520
+ } while (0)
521
+ #define SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail) \
522
+ do { \
523
+ ctrl_vec_t cv = ctrl_load((sh)->ctrl + (off)); \
524
+ SH_PREFETCH(&(sh)->slots[off]); \
525
+ (match) = ctrl_match_h2_vec(cv, (h2)); \
526
+ (empty) = ctrl_match_empty_vec(cv); \
527
+ (avail) = ctrl_match_empty_or_deleted_vec(cv); \
528
+ } while (0)
529
+ #else
530
+ #define SH_SCAN_GROUP(sh, off, h2, match, empty) \
531
+ do { \
532
+ SH_PREFETCH(&(sh)->slots[off]); \
533
+ (match) = ctrl_match_h2_raw((sh)->ctrl + (off), (h2)); \
534
+ (empty) = ctrl_match_empty_raw((sh)->ctrl + (off)); \
535
+ } while (0)
536
+ #define SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail) \
537
+ do { \
538
+ SH_PREFETCH(&(sh)->slots[off]); \
539
+ (match) = ctrl_match_h2_raw((sh)->ctrl + (off), (h2)); \
540
+ (empty) = ctrl_match_empty_raw((sh)->ctrl + (off)); \
541
+ (avail) = ctrl_match_empty_or_deleted_raw((sh)->ctrl + (off)); \
542
+ } while (0)
543
+ #endif
544
+
545
+ static inline VALUE *swiss_lookup_hashed(SwissHash *sh, VALUE key, uint64_t hash, int identity_only) {
415
546
  uint8_t h2 = H2(hash);
416
547
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
417
548
 
418
549
  for (;;) {
419
550
  size_t off = GROUP_OFF(ps.group_idx);
551
+ ctrl_mask_t match, empty;
552
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
420
553
 
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
554
  while (match) {
432
- int slot = ctz32(match);
555
+ int slot = CTRL_MASK_SLOT(match);
433
556
  Slot *s = &sh->slots[off + slot];
434
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
557
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
435
558
  return &s->value;
436
559
  }
437
560
  match &= match - 1;
@@ -443,23 +566,99 @@ static VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
443
566
  }
444
567
  }
445
568
 
569
+ static inline VALUE *swiss_lookup_string(SwissHash *sh, VALUE key, uint64_t hash) {
570
+ uint8_t h2 = H2(hash);
571
+ ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
572
+
573
+ for (;;) {
574
+ size_t off = GROUP_OFF(ps.group_idx);
575
+ ctrl_mask_t match, empty;
576
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
577
+
578
+ while (match) {
579
+ int slot = CTRL_MASK_SLOT(match);
580
+ Slot *s = &sh->slots[off + slot];
581
+ if (RB_LIKELY(s->key == key) || (RB_TYPE_P(s->key, T_STRING) && strings_equal(s->key, key))) {
582
+ return &s->value;
583
+ }
584
+ match &= match - 1;
585
+ }
586
+
587
+ if (empty)
588
+ return NULL;
589
+ probe_next(&ps);
590
+ }
591
+ }
592
+
593
+ typedef struct {
594
+ SwissHash *sh;
595
+ VALUE key;
596
+ VALUE value;
597
+ VALUE result;
598
+ VALUE *found;
599
+ } CallbackCtx;
600
+
601
+ SH_ALWAYS_INLINE int key_may_callback(VALUE key) {
602
+ return !(FIXNUM_P(key) || SYMBOL_P(key) || RB_TYPE_P(key, T_STRING));
603
+ }
604
+
605
+ static void swiss_lock(SwissHash *sh) {
606
+ if (sh->mutating) {
607
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
608
+ "(#hash or #eql? callback modified the same table)");
609
+ }
610
+ sh->mutating = 1;
611
+ }
612
+
613
+ static VALUE swiss_unlock_ensure(VALUE shv) {
614
+ ((SwissHash *)shv)->mutating = 0;
615
+ return Qnil;
616
+ }
617
+
618
+ static VALUE swiss_lookup_custom(VALUE arg) {
619
+ CallbackCtx *c = (CallbackCtx *)arg;
620
+ c->found = swiss_lookup_hashed(c->sh, c->key, compute_hash(c->key), 0);
621
+ return Qnil;
622
+ }
623
+
624
+ static inline VALUE *swiss_lookup(SwissHash *sh, VALUE key) {
625
+ if (FIXNUM_P(key)) {
626
+ return swiss_lookup_hashed(sh, key, hash_fixnum(key), 1);
627
+ }
628
+ if (SYMBOL_P(key)) {
629
+ return swiss_lookup_hashed(sh, key, hash_immediate(key), 1);
630
+ }
631
+ if (RB_TYPE_P(key, T_STRING)) {
632
+ return swiss_lookup_string(sh, key, hash_string(key));
633
+ }
634
+ if (sh->mutating) {
635
+ return swiss_lookup_hashed(sh, key, compute_hash(key), 0);
636
+ }
637
+ CallbackCtx ctx = {sh, key, Qnil, Qnil, NULL};
638
+ swiss_lock(sh);
639
+ rb_ensure(swiss_lookup_custom, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
640
+ return ctx.found;
641
+ }
642
+
446
643
  static void swiss_grow(SwissHash *sh);
447
644
  static void swiss_compact(SwissHash *sh);
448
645
 
449
- SH_ALWAYS_INLINE void swiss_insert_rehash(SwissHash *sh, uint64_t hash, VALUE key, VALUE value) {
646
+ SH_ALWAYS_INLINE void swiss_insert_rehash_into(uint8_t *ctrl, Slot *slots, size_t group_mask,
647
+ size_t *size, size_t *growth_left,
648
+ uint64_t hash, VALUE key, VALUE value) {
450
649
  uint8_t h2 = H2(hash);
451
- ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
650
+ ProbeSeq ps = probe_start(H1(hash), group_mask);
452
651
 
453
652
  for (;;) {
454
653
  size_t off = GROUP_OFF(ps.group_idx);
455
- uint32_t empty_mask = ctrl_match_empty(sh->ctrl + off);
654
+ ctrl_mask_t empty_mask = ctrl_match_empty(ctrl + off);
456
655
  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--;
656
+ size_t idx = off + (size_t)CTRL_MASK_SLOT(empty_mask);
657
+ ctrl[idx] = h2;
658
+ slots[idx].key = key;
659
+ slots[idx].value = value;
660
+ (*size)++;
661
+ (*growth_left)--;
463
662
  return;
464
663
  }
465
664
  probe_next(&ps);
@@ -470,44 +669,87 @@ static inline int should_compact(SwissHash *sh) {
470
669
  return sh->tombstone_count >= sh->capacity / TOMBSTONE_COMPACT_DIVISOR;
471
670
  }
472
671
 
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;
672
+ typedef struct {
673
+ SwissHash *sh;
674
+ uint8_t *new_ctrl;
675
+ Slot *new_slots;
676
+ const uint8_t *old_ctrl;
677
+ const Slot *old_slots;
678
+ size_t old_cap;
679
+ size_t new_cap;
680
+ size_t new_num_groups;
681
+ } RebuildCtx;
682
+
683
+ static VALUE swiss_rebuild_body(VALUE arg) {
684
+ RebuildCtx *ctx = (RebuildCtx *)arg;
685
+ size_t group_mask = ctx->new_num_groups - 1;
686
+ size_t size = 0;
687
+ size_t growth_left = ctx->new_cap * MAX_LOAD_NUM / MAX_LOAD_DEN;
688
+
689
+ for (size_t i = 0; i < ctx->old_cap; i++) {
690
+ uint8_t c = ctx->old_ctrl[i];
691
+ if (c != CTRL_EMPTY && c != CTRL_DELETED) {
692
+ VALUE key = ctx->old_slots[i].key;
693
+ uint64_t hash = FIXNUM_P(key) ? hash_fixnum(key) : compute_hash(key);
694
+ swiss_insert_rehash_into(ctx->new_ctrl, ctx->new_slots, group_mask,
695
+ &size, &growth_left, hash, key, ctx->old_slots[i].value);
696
+ }
697
+ }
479
698
 
480
- sh->ctrl = (uint8_t *)malloc(cap);
481
- sh->slots = (Slot *)malloc(cap * sizeof(Slot));
699
+ free(ctx->sh->ctrl);
700
+ ctx->sh->ctrl = ctx->new_ctrl;
701
+ ctx->sh->slots = ctx->new_slots;
702
+ ctx->sh->num_groups = ctx->new_num_groups;
703
+ ctx->sh->group_mask = group_mask;
704
+ ctx->sh->capacity = ctx->new_cap;
705
+ ctx->sh->size = size;
706
+ ctx->sh->growth_left = growth_left;
707
+ ctx->sh->tombstone_count = 0;
708
+ ctx->new_ctrl = NULL;
709
+ ctx->new_slots = NULL;
710
+ return Qnil;
711
+ }
482
712
 
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");
713
+ static VALUE swiss_rebuild_ensure(VALUE arg) {
714
+ RebuildCtx *ctx = (RebuildCtx *)arg;
715
+ if (ctx->new_ctrl) {
716
+ free(ctx->new_ctrl);
717
+ ctx->new_ctrl = NULL;
718
+ ctx->new_slots = NULL;
491
719
  }
720
+ return Qnil;
721
+ }
492
722
 
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;
723
+ static void swiss_rebuild(SwissHash *sh, size_t new_num_groups) {
724
+ RebuildCtx ctx;
725
+ memset(&ctx, 0, sizeof(ctx));
726
+ ctx.sh = sh;
727
+ ctx.old_ctrl = sh->ctrl;
728
+ ctx.old_slots = sh->slots;
729
+ ctx.old_cap = sh->capacity;
730
+ ctx.new_num_groups = new_num_groups;
731
+ ctx.new_cap = new_num_groups * GROUP_SIZE;
497
732
 
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
- }
733
+ if (!swiss_alloc_block(ctx.new_cap, &ctx.new_ctrl, &ctx.new_slots)) {
734
+ rb_raise(rb_eNoMemError, "failed to rebuild SwissHash");
504
735
  }
736
+ memset(ctx.new_ctrl, CTRL_EMPTY, ctx.new_cap);
737
+ rb_ensure(swiss_rebuild_body, (VALUE)&ctx, swiss_rebuild_ensure, (VALUE)&ctx);
738
+ }
505
739
 
506
- free(old_ctrl);
507
- free(old_slots);
740
+ SH_ALWAYS_INLINE void swiss_place_new(SwissHash *sh, size_t insert_idx, uint8_t h2, VALUE key, VALUE value) {
741
+ if (sh->ctrl[insert_idx] == CTRL_EMPTY) {
742
+ sh->growth_left--;
743
+ } else {
744
+ sh->tombstone_count--;
745
+ }
746
+ sh->ctrl[insert_idx] = h2;
747
+ sh->slots[insert_idx].key = key;
748
+ sh->slots[insert_idx].value = value;
749
+ sh->size++;
508
750
  }
509
751
 
510
- static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
752
+ static VALUE swiss_insert_hashed(SwissHash *sh, VALUE key, VALUE value, uint64_t hash, int identity_only, int may_callback) {
511
753
  if (sh->growth_left == 0) {
512
754
  if (should_compact(sh)) {
513
755
  swiss_compact(sh);
@@ -516,32 +758,33 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
516
758
  }
517
759
  }
518
760
 
519
- uint64_t hash = compute_hash(key);
520
761
  uint8_t h2 = H2(hash);
521
762
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
522
-
523
763
  size_t insert_idx = (size_t)-1;
524
764
 
525
765
  for (;;) {
526
766
  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);
767
+ ctrl_mask_t match, empty, avail;
768
+ #ifdef SWISS_USE_VEC
769
+ {
770
+ ctrl_vec_t cv = ctrl_load(sh->ctrl + off);
771
+ SH_PREFETCH(&sh->slots[off]);
772
+ match = ctrl_match_h2_vec(cv, h2);
773
+ empty = ctrl_match_empty_vec(cv);
774
+ avail = (sh->tombstone_count == 0) ? empty : ctrl_match_empty_or_deleted_vec(cv);
775
+ }
534
776
  #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);
777
+ SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail);
778
+ if (sh->tombstone_count == 0) {
779
+ avail = empty;
780
+ }
539
781
  #endif
782
+
540
783
  while (match) {
541
- int slot = ctz32(match);
784
+ int slot = CTRL_MASK_SLOT(match);
542
785
  size_t idx = off + slot;
543
786
  Slot *s = &sh->slots[idx];
544
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
787
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
545
788
  s->value = value;
546
789
  return value;
547
790
  }
@@ -549,7 +792,7 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
549
792
  }
550
793
 
551
794
  if (insert_idx == (size_t)-1 && avail) {
552
- insert_idx = off + ctz32(avail);
795
+ insert_idx = off + (size_t)CTRL_MASK_SLOT(avail);
553
796
  }
554
797
 
555
798
  if (empty)
@@ -557,108 +800,172 @@ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
557
800
  probe_next(&ps);
558
801
  }
559
802
 
560
- MUTATE_GUARD_BEGIN(sh);
803
+ swiss_place_new(sh, insert_idx, h2, key, value);
804
+ return value;
805
+ }
561
806
 
562
- if (sh->ctrl[insert_idx] == CTRL_EMPTY) {
563
- sh->growth_left--;
564
- } else {
565
- sh->tombstone_count--;
807
+ SH_ALWAYS_INLINE VALUE swiss_insert_fixnum(SwissHash *sh, VALUE key, VALUE value) {
808
+ if (RB_UNLIKELY(sh->growth_left == 0)) {
809
+ if (should_compact(sh)) {
810
+ swiss_compact(sh);
811
+ } else {
812
+ swiss_grow(sh);
813
+ }
566
814
  }
567
- sh->ctrl[insert_idx] = h2;
568
- sh->slots[insert_idx].key = key;
569
- sh->slots[insert_idx].value = value;
570
- sh->size++;
571
815
 
572
- MUTATE_GUARD_END(sh);
816
+ uint64_t hash = hash_fixnum(key);
817
+ uint8_t h2 = H2(hash);
818
+ ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
819
+ size_t insert_idx = (size_t)-1;
820
+
821
+ for (;;) {
822
+ size_t off = GROUP_OFF(ps.group_idx);
823
+ ctrl_mask_t match, empty, avail;
824
+ #ifdef SWISS_USE_VEC
825
+ ctrl_vec_t cv = ctrl_load(sh->ctrl + off);
826
+ SH_PREFETCH(&sh->slots[off]);
827
+ match = ctrl_match_h2_vec(cv, h2);
828
+ empty = ctrl_match_empty_vec(cv);
829
+ avail = (sh->tombstone_count == 0) ? empty : ctrl_match_empty_or_deleted_vec(cv);
830
+ #else
831
+ SH_SCAN_GROUP_INSERT(sh, off, h2, match, empty, avail);
832
+ if (sh->tombstone_count == 0) {
833
+ avail = empty;
834
+ }
835
+ #endif
836
+ while (match) {
837
+ int slot = CTRL_MASK_SLOT(match);
838
+ if (RB_LIKELY(sh->slots[off + slot].key == key)) {
839
+ sh->slots[off + slot].value = value;
840
+ return value;
841
+ }
842
+ match &= match - 1;
843
+ }
844
+
845
+ if (insert_idx == (size_t)-1 && avail) {
846
+ insert_idx = off + (size_t)CTRL_MASK_SLOT(avail);
847
+ }
848
+ if (RB_LIKELY(empty))
849
+ break;
850
+ probe_next(&ps);
851
+ }
852
+
853
+ swiss_place_new(sh, insert_idx, h2, key, value);
573
854
  return value;
574
855
  }
575
856
 
576
- static VALUE swiss_delete(SwissHash *sh, VALUE key) {
857
+ static VALUE swiss_insert_unlocked(VALUE arg) {
858
+ CallbackCtx *c = (CallbackCtx *)arg;
859
+ if (FIXNUM_P(c->key)) {
860
+ c->result = swiss_insert_fixnum(c->sh, c->key, c->value);
861
+ } else if (SYMBOL_P(c->key)) {
862
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, hash_immediate(c->key), 1, 0);
863
+ } else if (RB_TYPE_P(c->key, T_STRING)) {
864
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, hash_string(c->key), 0, 0);
865
+ } else {
866
+ c->result = swiss_insert_hashed(c->sh, c->key, c->value, compute_hash(c->key), 0, 0);
867
+ }
868
+ return c->result;
869
+ }
870
+
871
+ static VALUE swiss_insert(SwissHash *sh, VALUE key, VALUE value) {
872
+ if (sh->mutating) {
873
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
874
+ "(#hash or #eql? callback modified the same table)");
875
+ }
876
+
877
+ CallbackCtx ctx = {sh, key, value, Qnil, NULL};
878
+ if (!key_may_callback(key) && sh->growth_left != 0) {
879
+ return swiss_insert_unlocked((VALUE)&ctx);
880
+ }
881
+
882
+ swiss_lock(sh);
883
+ rb_ensure(swiss_insert_unlocked, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
884
+ return ctx.result;
885
+ }
886
+
887
+ static VALUE swiss_delete_unlocked(VALUE arg) {
888
+ CallbackCtx *c = (CallbackCtx *)arg;
889
+ SwissHash *sh = c->sh;
890
+ VALUE key = c->key;
577
891
  uint64_t hash = compute_hash(key);
892
+ int identity_only = FIXNUM_P(key) || SYMBOL_P(key);
578
893
  uint8_t h2 = H2(hash);
579
894
  ProbeSeq ps = probe_start(H1(hash), sh->group_mask);
580
895
 
581
896
  for (;;) {
582
897
  size_t off = GROUP_OFF(ps.group_idx);
898
+ ctrl_mask_t match, empty;
899
+ SH_SCAN_GROUP(sh, off, h2, match, empty);
583
900
 
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
901
  while (match) {
595
- int slot = ctz32(match);
902
+ int slot = CTRL_MASK_SLOT(match);
596
903
  size_t idx = off + slot;
597
904
  Slot *s = &sh->slots[idx];
598
- if (RB_LIKELY(s->key == key) || keys_equal(s->key, key)) {
905
+ if (RB_LIKELY(s->key == key) || (!identity_only && keys_equal(s->key, key))) {
599
906
  VALUE old_value = s->value;
600
-
601
- MUTATE_GUARD_BEGIN(sh);
602
907
  sh->ctrl[idx] = CTRL_DELETED;
603
908
  sh->slots[idx].key = Qnil;
604
909
  sh->slots[idx].value = Qnil;
605
910
  sh->size--;
606
911
  sh->tombstone_count++;
607
- MUTATE_GUARD_END(sh);
608
-
912
+ c->result = old_value;
609
913
  return old_value;
610
914
  }
611
915
  match &= match - 1;
612
916
  }
613
917
 
614
- if (empty)
918
+ if (empty) {
919
+ c->result = Qnil;
615
920
  return Qnil;
921
+ }
616
922
  probe_next(&ps);
617
923
  }
618
924
  }
619
925
 
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));
926
+ static VALUE swiss_delete(SwissHash *sh, VALUE key) {
927
+ if (sh->mutating) {
928
+ rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected "
929
+ "(#hash or #eql? callback modified the same table)");
930
+ }
632
931
 
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");
932
+ CallbackCtx ctx = {sh, key, Qnil, Qnil, NULL};
933
+ if (!key_may_callback(key)) {
934
+ return swiss_delete_unlocked((VALUE)&ctx);
641
935
  }
642
936
 
643
- memset(sh->ctrl, CTRL_EMPTY, new_cap);
937
+ swiss_lock(sh);
938
+ rb_ensure(swiss_delete_unlocked, (VALUE)&ctx, swiss_unlock_ensure, (VALUE)sh);
939
+ return ctx.result;
940
+ }
644
941
 
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;
942
+ typedef struct {
943
+ SwissHash *sh;
944
+ size_t new_num_groups;
945
+ } RebuildCall;
651
946
 
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
- }
947
+ static VALUE swiss_rebuild_call(VALUE arg) {
948
+ RebuildCall *c = (RebuildCall *)arg;
949
+ swiss_rebuild(c->sh, c->new_num_groups);
950
+ return Qnil;
951
+ }
952
+
953
+ static void swiss_rebuild_locked(SwissHash *sh, size_t new_num_groups) {
954
+ RebuildCall call = {sh, new_num_groups};
955
+ if (sh->mutating) {
956
+ swiss_rebuild(sh, new_num_groups);
957
+ return;
658
958
  }
959
+ swiss_lock(sh);
960
+ rb_ensure(swiss_rebuild_call, (VALUE)&call, swiss_unlock_ensure, (VALUE)sh);
961
+ }
962
+
963
+ static void swiss_grow(SwissHash *sh) {
964
+ swiss_rebuild_locked(sh, sh->num_groups * 2);
965
+ }
659
966
 
660
- free(old_ctrl);
661
- free(old_slots);
967
+ static void swiss_compact(SwissHash *sh) {
968
+ swiss_rebuild_locked(sh, sh->num_groups);
662
969
  }
663
970
 
664
971
  static void swiss_hash_mark(void *ptr) {
@@ -750,7 +1057,10 @@ static VALUE swiss_hash_initialize_copy(VALUE self, VALUE original) {
750
1057
 
751
1058
  static VALUE swiss_hash_aset(VALUE self, VALUE key, VALUE value) {
752
1059
  SwissHash *sh = (SwissHash *)RTYPEDDATA_DATA(self);
753
- if (RB_UNLIKELY(!(FIXNUM_P(key) || SYMBOL_P(key)))) {
1060
+ if (FIXNUM_P(key)) {
1061
+ return swiss_insert_fixnum(sh, key, value);
1062
+ }
1063
+ if (RB_UNLIKELY(!SYMBOL_P(key))) {
754
1064
  key = prepare_key(key);
755
1065
  }
756
1066
  return swiss_insert(sh, key, value);
@@ -758,7 +1068,12 @@ static VALUE swiss_hash_aset(VALUE self, VALUE key, VALUE value) {
758
1068
 
759
1069
  static VALUE swiss_hash_aref(VALUE self, VALUE key) {
760
1070
  SwissHash *sh = (SwissHash *)RTYPEDDATA_DATA(self);
761
- VALUE *val = swiss_lookup(sh, key);
1071
+ VALUE *val;
1072
+ if (FIXNUM_P(key)) {
1073
+ val = swiss_lookup_hashed(sh, key, hash_fixnum(key), 1);
1074
+ } else {
1075
+ val = swiss_lookup(sh, key);
1076
+ }
762
1077
  return val ? *val : Qnil;
763
1078
  }
764
1079
 
@@ -1401,6 +1716,8 @@ static VALUE swiss_hash_stats(VALUE self) {
1401
1716
 
1402
1717
  #ifdef SWISS_USE_SSE2
1403
1718
  rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("SSE2"));
1719
+ #elif defined(SWISS_USE_NEON)
1720
+ rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("NEON"));
1404
1721
  #else
1405
1722
  rb_hash_aset(hash, ID2SYM(rb_intern("simd")), rb_str_new_cstr("SWAR"));
1406
1723
  #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.3"
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.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haidarov