leerho opened a new pull request, #522:
URL: https://github.com/apache/datasketches-cpp/pull/522

   # Finish repairing the deferred KxQ rebuild, and pin the estimator constants
   
   Depends on #1 (`hll-full-size-reset`); this branch is cut from it because 
both touch
   `HllUtil.hpp` and `HllArray-internal.hpp`. Held as a draft until #1 merges.
   
   Three independent defects, all in the HLL estimator state.
   
   ## Part 1 — The problems
   
   ### P2. The deferred KxQ rebuild still leaves curMin/numAtCurMin merge-order 
dependent
   
   The lazy rebuild introduced in #364 defers recomputing KxQ and curMin after 
a merge. #512 repaired
   the visible damage — an empty-looking gadget discarding accumulated data, a 
corrupted HIP
   accumulator, order-dependent estimates — but not the representation the 
rebuild writes.
   
   `check_rebuild_kxq_cur_min()` stores the **true minimum register value** and 
the **count at that
   minimum**. The rest of the HLL_8 code maintains a different representation, 
the one documented in
   `HllArray-internal.hpp`: `curMin` is always 0 and `numAtCurMin` is the 
number of zero registers,
   decremented by `Hll8Array::internalCouponUpdate()` as registers fill.
   
   When the merged array has no zero registers the rebuild leaves `curMin > 0`, 
and from that point
   `numAtCurMin -= (curVal == 0)` never fires again. The stored pair freezes at 
whatever the rebuild
   computed and drifts away from the registers, so its value depends on 
**when** the rebuild fired —
   that is, on merge order.
   
       A = lg_k 12 HLL_4 keys [20000,30364)
       B = lg_k 10 HLL_8 keys [5000,14699)
       C = lg_k 17 HLL_4 keys [70000,72598)     // stays in SET mode
       hll_union u(7)
   
       order A,B,C -> curMin=6 numAtCurMin=11
       order B,C,A -> curMin=6 numAtCurMin=8
       registers identical, estimates identical (23040.86465)
       recomputed from the registers: curMin=6 numAtCurMin=8   <- A,B,C stores 
a wrong value
       4.0.1, before #364: both orders -> curMin=0 numAtCurMin=0, byte-identical
   
   Across 4800 randomized merge-order comparisons, 92 differed **only** in 
these two fields; the same
   sweep on the pre-#364 baseline gives 0. Estimates and bounds are unaffected, 
because both consumers
   (`getLowerBound`'s `numNonZeros` and `getHllBitMapEstimate`'s 
`numUnhitBuckets`) branch on
   `curMin == 0`. So this is a serialization-determinism defect, not an 
accuracy one.
   
   `check_rebuild_kxq_cur_min()` is also missing the `curMode == HLL && 
tgtHllType == HLL_8` guard
   that datasketches-java has. On an `Hll4Array` the rebuild would overwrite 
`curMin` without
   re-encoding the nibbles, which are stored relative to it. That is 
unreachable today — only
   `Hll8Array::mergeHll` sets the flag — but `setRebuildKxqCurminFlag()` and
   `check_rebuild_kxq_cur_min()` are both public.
   
   ### P4. The relative-error constants are rounded literals
   
       HLL_HIP_RSE_FACTOR     = 0.8325546   // sqrt(ln 2)          exact: 
0.8325546111576977
       HLL_NON_HIP_RSE_FACTOR = 1.03896     // sqrt(3 ln 2 - 1)    exact: 
1.0389617614136892
   
   datasketches-java computes these rather than rounding them. For `lg_k > 12` 
the bounds take the
   closed-form branch rather than the interpolation table, so the truncation is 
directly observable:
   the HIP bounds differ from Java by ~4.3e-11 relative, and the non-HIP factor 
is off by **1.7e-6
   relative**, which skews the bounds of every union result above `lg_k` 12.
   
   ### P5. log() comes from the platform libm
   
   `HarmonicNumbers::harmonicNumber` and `HllArray::getHllBitMapEstimate` call 
`std::log`.
   `getBitMapEstimate` computes `K * (H(K) - H(K - numHit))`, and the 
cancellation between two nearby
   harmonic numbers amplifies a 1 ULP difference in `log()` by roughly 17x. 
Apple libm differs from
   fdlibm on **0.62%** of a 400,000-sample sweep spanning subnormals to 1e300, 
always by 1 ULP.
   
   datasketches-java's `StrictMath.log` is specified to be fdlibm's 
`__ieee754_log`, and on the
   tested JVM `Math.log` is bit-identical to it over 3,000,000 assorted 
doubles. So Java is pinned and
   C++ is not — and C++ is not bit-stable against itself across platforms 
either.
   
   ## Part 2 — The fix
   
   - **`check_rebuild_kxq_cur_min()`** emits the canonical HLL_8 representation 
— `curMin = 0`,
     `numAtCurMin` = number of zero registers — so the rebuilt state is 
indistinguishable from the
     incrementally-maintained state and the timing of the rebuild is not 
observable. Adds the
     HLL_8 / HLL-mode guard.
   - **Full-precision RSE literals**, taken from Java's own `Double.toString()` 
output, with the
     defining expression in the comment. Literals rather than a runtime 
`sqrt(log(2.0))` so the value
     cannot vary with the platform libm.
   - **`common/include/fdlibm_log.hpp`** (new): FDLIBM 5.3 `__ieee754_log`, the 
function
     `StrictMath.log` is specified to be. Copyright (C) 1993 Sun Microsystems, 
freely distributable
     with the notice preserved; the notice is in the header. Used by 
`harmonicNumber` and
     `getHllBitMapEstimate`.
   
     fdlibm depends on strict IEEE-754 evaluation — a fused multiply-add in the 
polynomial changes the
     result. `#pragma clang fp contract(off)` is honoured under default flags 
but is overridden by an
     explicit `-ffp-contract=fast`, which is GCC's default, so the 
contraction-sensitive expressions
     are routed through a `volatile` round-trip that the standard requires the 
compiler to honour.
     Verified bit-identical to Java under `-O2`, `-O3`, `-ffp-contract=on` and 
`-ffp-contract=fast`.
     `-ffast-math` breaks it, as it breaks IEEE semantics generally.
   
   ## Compatibility
   
   Unlike #1, **this PR changes serialized bytes** — that is its purpose.
   
   - Union results: stored `curMin` / `numAtCurMin` change (to the canonical 
form 4.0.1 produced).
   - Estimates in the linear-counting range shift by ~1e-15 relative, from the 
`log` change.
   - Bounds at `lg_k > 12` shift by ~4.3e-11 (HIP) and ~1.7e-6 (non-HIP), from 
the constants.
   
   Golden-image tests and any test asserting exact double equality against 
previously recorded C++
   values will need updating. Reading is unaffected: images from every earlier 
version still
   deserialize, and both readers of `curMin`/`numAtCurMin` branch on `curMin == 
0`, so old and new
   images yield identical estimates and bounds.
   
   ## Part 3 — Tests
   
   New `hll/test/HllKxqRebuildTest.cpp` — 5 cases, 37 assertions:
   
   - a union result is byte-identical across all six permutations of three 
inputs, one of which stays
     in SET mode so the coupon-update path into a gadget with a pending rebuild 
is exercised;
   - reading an estimate mid-stream does not change a later `get_result()` 
image;
   - a union result's stored `curMin`/`numAtCurMin` match a recount over its 
own registers;
   - `get_rel_err()` matches `numStdDev * sqrt(ln 2) / sqrt(K)` and the non-HIP 
form at `lg_k`
     13/16/21 for 1..3 standard deviations;
   - `fdlibm::log` matches a nine-entry reference table on inputs where the 
platform libm diverges.
     The expected bit patterns were taken from Java's `StrictMath.log`.
   
   Of the four assertions that also compile against the parent branch, **three 
fail there and pass
   here**. The fourth — that reading an estimate does not change a later result 
— already held in
   C++, because #512's eager rebuild inside `internalCouponUpdate` fires in 
both the peeked and
   unpeeked orderings. It is kept as a guard, not claimed as a regression; that 
observer effect is
   the datasketches-java defect addressed in the companion PR.
   
   Full suite: 64 cases / 7758 assertions pass.
   
   ## Cross-language result
   
   With the companion datasketches-java PR applied, over 1041 records — `lg_k` 
4..21 x
   {HLL_4, HLL_6, HLL_8} x 17 sizes spanning LIST, SET and HLL modes, plus 
heapify round-trips and 80
   deterministic pseudo-random union scenarios:
   
       before:  428 of 1041 records differ
       after:     0 of 1041 records differ
   
   Zero differences in any field: both serialized forms, estimate, composite 
estimate, and lower and
   upper bounds at 1 and 2 standard deviations, compared as raw IEEE-754 bit 
patterns.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to