swiss_hash 0.1.3 → 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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +32 -33
- data/ext/swiss_hash/swiss_hash.c +34 -19
- data/lib/swiss_hash/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c2f1e2b2c9e7a8ee0ba2b4ab30f7088eeeefdd1e56abc6fb92758bd5a667a64
|
|
4
|
+
data.tar.gz: 92904e400f7f16702a48c2966afc8c41b4e05aacd769fbd3ff9abd54d8cd5b53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4a90125a2c8d4792299a7899331f14a0f285cb15fe497a5c53258641846f5811d8f07127cee8a3c0f698f35011a6a45190deeaa9149642fa36856ca04fc12fd
|
|
7
|
+
data.tar.gz: d93013782fc2df669c537d4f0907c2c398781ddf077afd79dc0ddce455f18476ba2e0dc798dd09637c21cb866eb4ceed06c63669b8e625c5f26c5ae48633ab55
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.1.3 - 2026-08-18
|
|
4
12
|
|
|
5
13
|
- NEON group matching on ARM64 (SSE2 on x86_64, SWAR elsewhere)
|
data/README.md
CHANGED
|
@@ -27,64 +27,63 @@ 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:
|
|
30
|
+
Methodology: 10 runs × 21 measured iterations, 5 warmup iterations per run, IQR-filtered mean per run, interleaved Ruby/SwissHash measurements with alternating start order, and per-side coefficient of variation (`±X.X%`) reported to make noise visible.
|
|
31
31
|
|
|
32
32
|
### N = 100,000
|
|
33
33
|
|
|
34
34
|
| Operation | Ruby Hash | SwissHash | Delta |
|
|
35
35
|
|---|---:|---:|---:|
|
|
36
|
-
| Insert (sequential int) |
|
|
37
|
-
| Insert (string keys) |
|
|
38
|
-
| Insert (random int) |
|
|
39
|
-
| Lookup (sequential int, 3x) | 12.
|
|
40
|
-
| Lookup (string keys, 3x) |
|
|
41
|
-
| Delete + reinsert 25% |
|
|
42
|
-
| Mixed (70
|
|
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.
|
|
49
|
-
| Insert (string keys) | 1.
|
|
50
|
-
| Insert (random int) | 0.
|
|
51
|
-
| Lookup (sequential int, 3x) | 1.
|
|
52
|
-
| Lookup (string keys, 3x) | 1.
|
|
53
|
-
| Delete + reinsert 25% | 0.
|
|
54
|
-
| Mixed (70
|
|
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 be close. String-heavy workloads remain the strongest SwissHash case.
|
|
59
|
-
|
|
60
58
|
| Operation | Ruby Hash | SwissHash | Delta |
|
|
61
59
|
|---|---:|---:|---:|
|
|
62
|
-
| Insert (sequential int) | 0.
|
|
63
|
-
| Insert (string keys) | 0.
|
|
64
|
-
| Insert (random int) | 0.
|
|
65
|
-
| Lookup (sequential int, 3x) | 0.
|
|
66
|
-
| Lookup (string keys, 3x) | 0.
|
|
67
|
-
| Delete + reinsert 25% | 0.
|
|
68
|
-
| Mixed (70
|
|
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
|
|
73
|
-
- The strongest win is
|
|
74
|
-
- Sequential integer insert at N=1k now beats Ruby Hash (it lost slightly in 0.1.2).
|
|
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.
|
|
75
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
|
|
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 |
|
|
83
|
-
|
|
84
|
-
| SwissHash | 2,176
|
|
85
|
-
|
|
|
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
|
-
|
|
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
|
|
data/ext/swiss_hash/swiss_hash.c
CHANGED
|
@@ -451,32 +451,30 @@ static void swiss_free_arrays(SwissHash *sh) {
|
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
static void swiss_init(SwissHash *sh, size_t min_capacity) {
|
|
454
|
-
|
|
455
|
-
swiss_free_arrays(sh);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
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);
|
|
459
455
|
size_t num_groups = 1;
|
|
460
|
-
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
|
+
}
|
|
461
460
|
num_groups <<= 1;
|
|
461
|
+
}
|
|
462
462
|
if (num_groups < 2)
|
|
463
463
|
num_groups = 2;
|
|
464
464
|
|
|
465
|
+
if (num_groups > SIZE_MAX / GROUP_SIZE) {
|
|
466
|
+
rb_raise(rb_eArgError, "SwissHash capacity is too large");
|
|
467
|
+
}
|
|
465
468
|
size_t capacity = num_groups * GROUP_SIZE;
|
|
466
469
|
uint8_t *ctrl = NULL;
|
|
467
470
|
Slot *slots = NULL;
|
|
468
471
|
|
|
469
472
|
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
473
|
rb_raise(rb_eNoMemError, "failed to allocate SwissHash");
|
|
478
474
|
}
|
|
479
475
|
|
|
476
|
+
memset(ctrl, CTRL_EMPTY, capacity);
|
|
477
|
+
uint8_t *old_ctrl = sh->ctrl;
|
|
480
478
|
sh->ctrl = ctrl;
|
|
481
479
|
sh->slots = slots;
|
|
482
480
|
sh->num_groups = num_groups;
|
|
@@ -486,7 +484,7 @@ static void swiss_init(SwissHash *sh, size_t min_capacity) {
|
|
|
486
484
|
sh->tombstone_count = 0;
|
|
487
485
|
sh->mutating = 0;
|
|
488
486
|
sh->growth_left = capacity * MAX_LOAD_NUM / MAX_LOAD_DEN;
|
|
489
|
-
|
|
487
|
+
free(old_ctrl);
|
|
490
488
|
}
|
|
491
489
|
|
|
492
490
|
typedef struct {
|
|
@@ -1019,6 +1017,10 @@ static VALUE swiss_hash_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
1019
1017
|
SwissHash *sh;
|
|
1020
1018
|
TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, sh);
|
|
1021
1019
|
|
|
1020
|
+
if (sh->mutating) {
|
|
1021
|
+
rb_raise(rb_eRuntimeError, "SwissHash: reentrant modification detected");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1022
1024
|
size_t capacity = NIL_P(capacity_val) ? 16 : NUM2SIZET(capacity_val);
|
|
1023
1025
|
swiss_init(sh, capacity);
|
|
1024
1026
|
|
|
@@ -1035,10 +1037,6 @@ static VALUE swiss_hash_initialize_copy(VALUE self, VALUE original) {
|
|
|
1035
1037
|
TypedData_Get_Struct(original, SwissHash, &swiss_hash_type, src);
|
|
1036
1038
|
TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, dst);
|
|
1037
1039
|
|
|
1038
|
-
if (dst->ctrl || dst->slots) {
|
|
1039
|
-
swiss_free_arrays(dst);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1040
|
if (!src->ctrl || !src->slots) {
|
|
1043
1041
|
swiss_init(dst, 16);
|
|
1044
1042
|
return self;
|
|
@@ -1057,6 +1055,10 @@ static VALUE swiss_hash_initialize_copy(VALUE self, VALUE original) {
|
|
|
1057
1055
|
|
|
1058
1056
|
static VALUE swiss_hash_aset(VALUE self, VALUE key, VALUE value) {
|
|
1059
1057
|
SwissHash *sh = (SwissHash *)RTYPEDDATA_DATA(self);
|
|
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
|
+
}
|
|
1060
1062
|
if (FIXNUM_P(key)) {
|
|
1061
1063
|
return swiss_insert_fixnum(sh, key, value);
|
|
1062
1064
|
}
|
|
@@ -1098,6 +1100,11 @@ static VALUE swiss_hash_clear(VALUE self) {
|
|
|
1098
1100
|
SwissHash *sh;
|
|
1099
1101
|
TypedData_Get_Struct(self, SwissHash, &swiss_hash_type, sh);
|
|
1100
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
|
+
|
|
1101
1108
|
memset(sh->ctrl, CTRL_EMPTY, sh->capacity);
|
|
1102
1109
|
memset(sh->slots, 0, sh->capacity * sizeof(Slot));
|
|
1103
1110
|
sh->size = 0;
|
|
@@ -1372,8 +1379,16 @@ static VALUE swiss_hash_merge(int argc, VALUE *argv, VALUE self) {
|
|
|
1372
1379
|
}
|
|
1373
1380
|
|
|
1374
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
|
+
}
|
|
1375
1390
|
swiss_hash_clear(self);
|
|
1376
|
-
VALUE argv[1] = {other};
|
|
1391
|
+
VALUE argv[1] = {NIL_P(converted) ? other : converted};
|
|
1377
1392
|
swiss_hash_merge_bang(1, argv, self);
|
|
1378
1393
|
return self;
|
|
1379
1394
|
}
|
data/lib/swiss_hash/version.rb
CHANGED