deepdiff-rs 0.5.3__tar.gz → 0.5.5__tar.gz
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.
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/Cargo.lock +3 -3
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/Cargo.toml +1 -1
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/PKG-INFO +1 -1
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/memo.rs +26 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/tests.rs +120 -119
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/README.md +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/Cargo.toml +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/examples/stack_frame_cost.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/datetime.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/datetime_tests.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/array.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/dispatch.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/mod.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/object.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/options.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/scalar.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/set.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/diff/tests.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/error.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/distance.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/fxhash.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/hash.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/mod.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/ignore_order/pairing.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/lcs.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/lcs_tests.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/lib.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/path.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/report.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/report_tests.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/test_support.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/value.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/src/value_tests.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/tests/golden.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/tests/ignore_order_memory.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/tests/memory_footprint.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/tests/proptest_diff.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-core/tests/proptest_ignore_order.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/Cargo.toml +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/benchmarks/bench_bindings.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/convert.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/deepdiff.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/errors.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/fast_path.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/guard.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/src/lib.rs +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_bindings_memory.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_conversions.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_datetimes.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_depth_guard.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_differential_fuzz.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_golden_parity.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_sets.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_signed_zero.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_smoke.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_suite_hygiene.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/crates/onix-py/tests/test_tuples.py +0 -0
- {deepdiff_rs-0.5.3 → deepdiff_rs-0.5.5}/pyproject.toml +0 -0
|
@@ -127,7 +127,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
|
127
127
|
|
|
128
128
|
[[package]]
|
|
129
129
|
name = "onix-cli"
|
|
130
|
-
version = "0.5.
|
|
130
|
+
version = "0.5.5"
|
|
131
131
|
dependencies = [
|
|
132
132
|
"onix-core",
|
|
133
133
|
"serde_json",
|
|
@@ -135,7 +135,7 @@ dependencies = [
|
|
|
135
135
|
|
|
136
136
|
[[package]]
|
|
137
137
|
name = "onix-core"
|
|
138
|
-
version = "0.5.
|
|
138
|
+
version = "0.5.5"
|
|
139
139
|
dependencies = [
|
|
140
140
|
"proptest",
|
|
141
141
|
"serde",
|
|
@@ -145,7 +145,7 @@ dependencies = [
|
|
|
145
145
|
|
|
146
146
|
[[package]]
|
|
147
147
|
name = "onix-py"
|
|
148
|
-
version = "0.5.
|
|
148
|
+
version = "0.5.5"
|
|
149
149
|
dependencies = [
|
|
150
150
|
"onix-core",
|
|
151
151
|
"pyo3",
|
|
@@ -179,6 +179,15 @@ pub(crate) struct IgnoreOrderMemo {
|
|
|
179
179
|
/// [`BTreeMap`] for the same collision-immunity reason as `node_table`.
|
|
180
180
|
member_content: RefCell<BTreeMap<MemberContent, RepId>>,
|
|
181
181
|
enabled: bool,
|
|
182
|
+
/// Total number of times [`Self::put`] has actually run — every distance
|
|
183
|
+
/// *recomputation*, not just the distinct entries it leaves behind (a
|
|
184
|
+
/// repeated `put` for a key already in `cache` overwrites the entry
|
|
185
|
+
/// rather than growing [`Self::cache_len`], so this is the only signal
|
|
186
|
+
/// that would rise if a caller re-derived a distance it should have
|
|
187
|
+
/// gotten from [`Self::get`] instead). Test-only: see
|
|
188
|
+
/// [`Self::put_count`]'s own doc for what it guards.
|
|
189
|
+
#[cfg(test)]
|
|
190
|
+
puts: std::cell::Cell<usize>,
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
impl IgnoreOrderMemo {
|
|
@@ -191,6 +200,8 @@ impl IgnoreOrderMemo {
|
|
|
191
200
|
node_table: RefCell::new(BTreeMap::new()),
|
|
192
201
|
member_content: RefCell::new(BTreeMap::new()),
|
|
193
202
|
enabled: true,
|
|
203
|
+
#[cfg(test)]
|
|
204
|
+
puts: std::cell::Cell::new(0),
|
|
194
205
|
}
|
|
195
206
|
}
|
|
196
207
|
|
|
@@ -206,6 +217,7 @@ impl IgnoreOrderMemo {
|
|
|
206
217
|
node_table: RefCell::new(BTreeMap::new()),
|
|
207
218
|
member_content: RefCell::new(BTreeMap::new()),
|
|
208
219
|
enabled: false,
|
|
220
|
+
puts: std::cell::Cell::new(0),
|
|
209
221
|
}
|
|
210
222
|
}
|
|
211
223
|
|
|
@@ -229,6 +241,18 @@ impl IgnoreOrderMemo {
|
|
|
229
241
|
self.cache.borrow().len()
|
|
230
242
|
}
|
|
231
243
|
|
|
244
|
+
/// The number of times [`Self::put`] has run — see that counter's own
|
|
245
|
+
/// field doc. A deep, self-similar nesting whose distance is computed
|
|
246
|
+
/// through this cache must see this count grow *linearly* with depth: a
|
|
247
|
+
/// cache whose lookup is broken (every candidate pair is recomputed
|
|
248
|
+
/// rather than reused) instead compounds `~2x` per level, which this
|
|
249
|
+
/// deterministically catches without timing anything — see
|
|
250
|
+
/// `super::tests::deep_nested_ignore_order_memoizes_distance_computations_linearly`.
|
|
251
|
+
#[cfg(test)]
|
|
252
|
+
pub(crate) fn put_count(&self) -> usize {
|
|
253
|
+
self.puts.get()
|
|
254
|
+
}
|
|
255
|
+
|
|
232
256
|
/// The cached distance for `key`, if present.
|
|
233
257
|
pub(crate) fn get(&self, key: &DistanceKey) -> Option<f64> {
|
|
234
258
|
self.cache.borrow().get(key).copied()
|
|
@@ -236,6 +260,8 @@ impl IgnoreOrderMemo {
|
|
|
236
260
|
|
|
237
261
|
/// Records `value` for `key` (moving the already-cloned key in).
|
|
238
262
|
pub(crate) fn put(&self, key: DistanceKey, value: f64) {
|
|
263
|
+
#[cfg(test)]
|
|
264
|
+
self.puts.set(self.puts.get() + 1);
|
|
239
265
|
self.cache.borrow_mut().insert(key, value);
|
|
240
266
|
}
|
|
241
267
|
|
|
@@ -1332,15 +1332,25 @@ proptest! {
|
|
|
1332
1332
|
}
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1335
|
+
/// A memoized deep-nested `ignore_order` diff must recompute each level's
|
|
1336
|
+
/// pairing distance exactly once — replaces a wall-clock guard that was
|
|
1337
|
+
/// flaky under parallel CI (issue #33). Measured directly instead, with no
|
|
1338
|
+
/// clock in the loop:
|
|
1339
|
+
///
|
|
1340
|
+
/// [`super::pairing::compute_pairs`] recomputes a container pair's distance
|
|
1341
|
+
/// only on an [`IgnoreOrderMemo`] cache miss (see [`IgnoreOrderMemo::put`]'s
|
|
1342
|
+
/// field doc), so [`IgnoreOrderMemo::put_count`] — every recomputation, not
|
|
1343
|
+
/// just the distinct entries a repeated `put` leaves behind — is a direct,
|
|
1344
|
+
/// deterministic stand-in for the "re-diffs each level twice, compounding
|
|
1345
|
+
/// `~2x` per level" cost the timing bound used to catch. A single-element
|
|
1346
|
+
/// nested list of depth `d` has exactly `d - 1` container-pair candidates
|
|
1347
|
+
/// (the outermost wrapper is never itself paired against anything), so a
|
|
1348
|
+
/// working memo leaves `put_count() == depth - 1`; confirmed by temporarily
|
|
1349
|
+
/// forcing [`IgnoreOrderMemo::get`] to always return `None` and re-running
|
|
1350
|
+
/// this test, which then reported `2^(depth - 1) - 1` puts (8,191 at depth
|
|
1351
|
+
/// 14 alone, where the working memo leaves 13) instead of failing on a clock.
|
|
1352
|
+
#[test]
|
|
1353
|
+
fn deep_nested_ignore_order_memoizes_distance_computations_linearly() {
|
|
1344
1354
|
let opts = DiffOptions {
|
|
1345
1355
|
ignore_order: true,
|
|
1346
1356
|
max_depth: 100_000,
|
|
@@ -1353,28 +1363,31 @@ fn deep_nested_ignore_order_completes_quickly_with_memoization() {
|
|
|
1353
1363
|
crate::value::Value::from(value)
|
|
1354
1364
|
};
|
|
1355
1365
|
|
|
1356
|
-
let
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
!
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
);
|
|
1366
|
+
let recomputations_at = |depth: usize| -> usize {
|
|
1367
|
+
let (a, b) = (build(depth, 1), build(depth, 2));
|
|
1368
|
+
let memo = IgnoreOrderMemo::new();
|
|
1369
|
+
let report = crate::diff::diff_with_options_memo(&a, &b, &opts, &memo)
|
|
1370
|
+
.unwrap_or_else(|e| panic!("depth-{depth} diffs: {e}"));
|
|
1371
|
+
assert!(
|
|
1372
|
+
!report.is_empty(),
|
|
1373
|
+
"depth-{depth} unequal input must report a change"
|
|
1374
|
+
);
|
|
1375
|
+
memo.put_count()
|
|
1376
|
+
};
|
|
1368
1377
|
|
|
1369
|
-
// Depth 25 previously hung
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
+
// Depth 25 previously hung for tens of seconds unmemoized; both depths
|
|
1379
|
+
// are kept so a regression shows up well before it would need to.
|
|
1380
|
+
for depth in [20usize, 25] {
|
|
1381
|
+
let puts = recomputations_at(depth);
|
|
1382
|
+
assert_eq!(
|
|
1383
|
+
puts,
|
|
1384
|
+
depth - 1,
|
|
1385
|
+
"depth-{depth} nested ignore_order recomputed {puts} distances, \
|
|
1386
|
+
expected exactly {} (one per level) — the distance memo is not \
|
|
1387
|
+
being hit",
|
|
1388
|
+
depth - 1
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1378
1391
|
}
|
|
1379
1392
|
|
|
1380
1393
|
// --- tuples under ignore_order -------------------------------------------
|
|
@@ -2487,100 +2500,88 @@ fn a_deeply_nested_set_member_hashes_and_compares_without_native_recursion() {
|
|
|
2487
2500
|
.expect("set-member hashing and comparison complete on a small stack");
|
|
2488
2501
|
}
|
|
2489
2502
|
|
|
2490
|
-
/// Interning `K` set members must
|
|
2491
|
-
///
|
|
2492
|
-
///
|
|
2493
|
-
///
|
|
2494
|
-
/// bits; on the `FxHash` tables the crate keeps (e.g. `HashedList` for an
|
|
2495
|
-
/// `ignore_order` list), a run of them collides unless the float bits are
|
|
2496
|
-
/// mixed first ([`crate::lcs::mix_float_bits`]). Reverting the mixing turns
|
|
2497
|
-
/// the float rows here red, so they genuinely guard it.
|
|
2498
|
-
/// - **Benign near-linearity (`SetIntPair`).** A run of plain int 2-tuples
|
|
2499
|
-
/// exercises the set-member tables on the *shape* of the crafted
|
|
2500
|
-
/// hash-flooding attack, but does **not** stand in for the attack: sequential
|
|
2501
|
-
/// ints do not collide under `FxHash`, so this row stays green even if the
|
|
2502
|
-
/// tables were reverted to `FxHash`. The adversarial hazard — the tables are
|
|
2503
|
-
/// keyed by attacker-controlled content and reached with the default
|
|
2504
|
-
/// `ignore_order=false` — is guarded at the **type level** instead: the tables
|
|
2505
|
-
/// are [`BTreeMap`]s, and [`super::hash::MemberHashKey`]/
|
|
2506
|
-
/// [`super::hash::MemberContent`] no longer derive `Hash`, so putting them
|
|
2507
|
-
/// back on an `FxHash` map fails to compile (`E0599`). This row is a plain
|
|
2508
|
-
/// regression check that the `BTreeMap` path itself scales.
|
|
2503
|
+
/// Interning `K` set/list members must never collapse onto one hash bucket —
|
|
2504
|
+
/// replaces a wall-clock `K -> 2K` diff-time ratio that was flaky under
|
|
2505
|
+
/// parallel CI (issue #33). Measured directly instead, with no diff and no
|
|
2506
|
+
/// clock in the loop:
|
|
2509
2507
|
///
|
|
2510
|
-
///
|
|
2511
|
-
/// `
|
|
2512
|
-
///
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
match shape {
|
|
2533
|
-
Shape::SetIntFloat => CValue::Tuple(vec![f(n as f64)].into_boxed_slice()),
|
|
2534
|
-
Shape::SetHalfFloat => CValue::Tuple(vec![f(n as f64 + 0.5)].into_boxed_slice()),
|
|
2535
|
-
Shape::SetIntPair => CValue::Tuple(vec![i(n_i), i(n_i)].into_boxed_slice()),
|
|
2536
|
-
Shape::ListFloat => f(n as f64),
|
|
2537
|
-
}
|
|
2538
|
-
};
|
|
2539
|
-
let side = |extra: bool| {
|
|
2540
|
-
let mut items: Vec<CValue> = (0..k).map(member).collect();
|
|
2541
|
-
if extra {
|
|
2542
|
-
items.push(CValue::Str("sentinel".to_string().into_boxed_str()));
|
|
2543
|
-
}
|
|
2544
|
-
match shape {
|
|
2545
|
-
Shape::ListFloat => CValue::Array(items.into_boxed_slice()),
|
|
2546
|
-
_ => CValue::Set(SetItems::new(items)),
|
|
2547
|
-
}
|
|
2548
|
-
};
|
|
2549
|
-
let opts = DiffOptions {
|
|
2550
|
-
ignore_order: matches!(shape, Shape::ListFloat),
|
|
2551
|
-
..DiffOptions::default()
|
|
2552
|
-
};
|
|
2553
|
-
// Differ by one member so the whole-value fast path can't short-circuit.
|
|
2554
|
-
(side(false), side(true), opts)
|
|
2555
|
-
};
|
|
2508
|
+
/// [`super::hash::item_key`]'s `Float` arm hashes through
|
|
2509
|
+
/// [`crate::lcs::mix_float_bits`] before the bits ever reach one of this
|
|
2510
|
+
/// module's `FxHash` tables (e.g. [`super::hash::HashedList`], the table an
|
|
2511
|
+
/// `ignore_order` list's items are matched through) — an integral or
|
|
2512
|
+
/// half-integer float's raw bit pattern shares dozens of trailing zero bits
|
|
2513
|
+
/// over the range this test uses, and `hashbrown` picks a table's bucket
|
|
2514
|
+
/// from a hash's *low* bits (see `mix_float_bits`'s own doc), so an unmixed
|
|
2515
|
+
/// run of them would all name the same bucket and degrade every lookup to a
|
|
2516
|
+
/// linear scan. This hashes real [`super::hash::ItemKey`]s through the real
|
|
2517
|
+
/// [`FxHasher`] `HashedList` itself uses and checks the low 32 bits (a proxy
|
|
2518
|
+
/// wide enough that no realistic table capacity at these sizes reads outside
|
|
2519
|
+
/// it) of `K`, and separately `2K`, keys: with the mixing intact they land in
|
|
2520
|
+
/// (almost) pairwise-distinct buckets, so the distinct count grows linearly
|
|
2521
|
+
/// with `K`; reverting or breaking the mix collapses the whole run onto a
|
|
2522
|
+
/// handful of buckets regardless of `K`, which the linearity assertion below
|
|
2523
|
+
/// catches immediately (confirmed by temporarily reverting `hash.rs`'s
|
|
2524
|
+
/// `Float` arm to hash the raw bits and re-running this test, which then
|
|
2525
|
+
/// fails on both the distinctness and the growth checks).
|
|
2526
|
+
#[test]
|
|
2527
|
+
fn float_hash_buckets_stay_distinct_and_grow_linearly_with_member_count() {
|
|
2528
|
+
use std::collections::HashSet;
|
|
2529
|
+
use std::hash::{Hash, Hasher};
|
|
2556
2530
|
|
|
2557
|
-
let
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2531
|
+
let low_bucket = |n: usize, half: bool| -> u32 {
|
|
2532
|
+
#[allow(
|
|
2533
|
+
clippy::cast_precision_loss,
|
|
2534
|
+
reason = "n stays well under 2^53 here; exactness is not the point of the probe"
|
|
2535
|
+
)]
|
|
2536
|
+
let value = n as f64 + if half { 0.5 } else { 0.0 };
|
|
2537
|
+
let key = item_key(&json!(value));
|
|
2538
|
+
let mut hasher = FxHasher::default();
|
|
2539
|
+
key.hash(&mut hasher);
|
|
2540
|
+
#[allow(
|
|
2541
|
+
clippy::cast_possible_truncation,
|
|
2542
|
+
reason = "only the low 32 bits are the bucket proxy under test"
|
|
2543
|
+
)]
|
|
2544
|
+
{
|
|
2545
|
+
hasher.finish() as u32
|
|
2546
|
+
}
|
|
2567
2547
|
};
|
|
2568
2548
|
|
|
2569
|
-
for (name,
|
|
2570
|
-
("set int-float", Shape::SetIntFloat),
|
|
2571
|
-
("set half-float", Shape::SetHalfFloat),
|
|
2572
|
-
("set int-pair", Shape::SetIntPair),
|
|
2573
|
-
("ignore_order list float", Shape::ListFloat),
|
|
2574
|
-
] {
|
|
2549
|
+
for (name, half) in [("integral", false), ("half-integer", true)] {
|
|
2575
2550
|
let k = 10_000;
|
|
2576
|
-
let
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
);
|
|
2551
|
+
let distinct_count = |count: usize| -> usize {
|
|
2552
|
+
(0..count)
|
|
2553
|
+
.map(|n| low_bucket(n, half))
|
|
2554
|
+
.collect::<HashSet<_>>()
|
|
2555
|
+
.len()
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
let distinct_1x = distinct_count(k);
|
|
2559
|
+
let distinct_2x = distinct_count(2 * k);
|
|
2560
|
+
|
|
2561
|
+
#[allow(
|
|
2562
|
+
clippy::cast_precision_loss,
|
|
2563
|
+
reason = "k is a small test constant; the ratio only needs two significant figures"
|
|
2564
|
+
)]
|
|
2565
|
+
{
|
|
2566
|
+
assert!(
|
|
2567
|
+
(distinct_1x as f64) > 0.99 * (k as f64),
|
|
2568
|
+
"{name} floats: only {distinct_1x}/{k} distinct hash buckets at K \
|
|
2569
|
+
— keys are colliding in their interning table"
|
|
2570
|
+
);
|
|
2571
|
+
assert!(
|
|
2572
|
+
(distinct_2x as f64) > 0.99 * (2.0 * k as f64),
|
|
2573
|
+
"{name} floats: only {distinct_2x}/{} distinct hash buckets at 2K \
|
|
2574
|
+
— keys are colliding in their interning table",
|
|
2575
|
+
2 * k
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
let ratio = distinct_2x as f64 / distinct_1x as f64;
|
|
2579
|
+
assert!(
|
|
2580
|
+
(1.9..=2.1).contains(&ratio),
|
|
2581
|
+
"{name} floats: distinct-bucket count did not grow linearly \
|
|
2582
|
+
with K (K->2K ratio {ratio:.3}, distinct_1x={distinct_1x}, distinct_2x={distinct_2x})"
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2584
2585
|
}
|
|
2585
2586
|
}
|
|
2586
2587
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|