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

   # Make the deferred KxQ rebuild canonical, stop HIP drifting after a merge, 
set the compact flag for HLL_6/HLL_8
   
   Companion to two datasketches-cpp PRs. Together these bring the two 
implementations to
   byte-for-byte agreement; the C++ side carries the matching changes.
   
   ## Part 1 — The problems
   
   ### J1. The deferred rebuild writes a state the incremental path does not 
maintain
   
   `HllUnion.checkRebuildCurMinNumKxQ` recomputes the deferred state and stores 
the **true minimum
   register value** and the **count at that minimum**. But 
`Hll8Array.updateSlotWithKxQ` maintains a
   different representation — the one its own comment describes, `//interpret 
numAtCurMin as num
   Zeros` — decrementing `numAtCurMin` only when `oldValue == 0`.
   
   When the merged array has no zero registers, the rebuild leaves `curMin > 
0`, and from that point
   `numAtCurMin` is never maintained again. It freezes at whatever the rebuild 
computed and drifts
   away from the registers.
   
   Because the rebuild is triggered lazily by accessors, this is directly 
observable: **merely calling
   `getEstimate()` changes the bytes a later `getResult()` produces.**
   
       P = lgK 13 HLL_8 keys [0,50000)
       Q = lgK 13 HLL_8 keys [50000,100000)
   
       HllUnion u = new HllUnion(lgMaxK);
       u.update(P); u.update(Q);
       [ u.getEstimate(); ]                        // <-- with vs without this 
line
       for (long v = 9_000_000; v < 9_400_000; v++) u.update(v);
       u.getResult(HLL_8).toUpdatableByteArray();
   
       lgMaxK=7   with getEstimate(): curMin=7  numAtCurMin=1
                  without:            curMin=10 numAtCurMin=4     <- and this 
is the correct value
       lgMaxK=8   with:               curMin=6  numAtCurMin=2
                  without:            curMin=9  numAtCurMin=8
       lgMaxK=9   with:               curMin=5  numAtCurMin=1
                  without:            curMin=8  numAtCurMin=9
   
       registers identical, estimates identical in every case
   
   Estimates and bounds are unaffected: both consumers of `numAtCurMin`
   (`HllEstimators.hllLowerBound`'s `numNonZeros` and `getHllBitMapEstimate`'s 
`numUnhitBuckets`)
   branch on `curMin == 0`, where the zero-count bookkeeping is exact. So this 
is a
   serialization-determinism defect, not an accuracy one.
   
   ### J2. The HIP accumulator keeps drifting after the out-of-order flag is set
   
   `HllArray.putOutOfOrder(true)` correctly zeroes `hipAccum`, but
   `AbstractHllArray.hipAndKxQIncrementalUpdate` calls 
`host.addToHipAccum(...)` unconditionally. So
   every coupon applied to a union gadget after a merge keeps accumulating into 
a field that is dead:
   once out-of-order is set, `getEstimate()` returns the composite estimate and 
`hipAccum` is never
   read again.
   
   The value it reaches is not even a function of the sketch content, because 
while the rebuild flag
   is pending the increment `K / (kxq0 + kxq1)` is computed against the 
empty-sketch KxQ defaults:
   
       two lgK 13 sketches merged into HllUnion(7), then 400k scalar updates, 
out-of-order = true:
          without an intervening getEstimate():  hipAccum = 131328
          with:                                  hipAccum = 396579
          datasketches-cpp in both cases:        hipAccum = 0
   
   This is also what makes a union result's byte image merge-order dependent.
   
   ### J3. The compact flag is not set for HLL_6 and HLL_8
   
   `HllArray.toCompactByteArray()` returns `toUpdatableByteArray()` — 
"indistinguishable for HLL6 and
   HLL8" — so the flag is never set for those two types, while LIST, SET and 
HLL_4 all set it. The
   project's own test documents the split:
   
       //LIST:  follows the toByteArray request
       //SET:   follows the toByteArray request
       //HLL8:  always updatable
       //HLL6:  always updatable
       //HLL:4  follows the toByteArray request
   
   The flag carries two meanings: the data is compacted where that is possible, 
and the image is
   immutable. The first genuinely does not apply to HLL_6 and HLL_8, which have 
no auxiliary table to
   compact — but the second applies to every target type. A user who calls 
`toCompactByteArray()` and
   finds the flag clear has cause for alarm, and datasketches-cpp sets it for 
all types, so the two
   implementations disagree about the same sketch.
   
   ### J4. The relative-error constants are computed at runtime
   
       HLL_HIP_RSE_FACTOR     = sqrt(log(2.0))
       HLL_NON_HIP_RSE_FACTOR = sqrt((3.0 * log(2.0)) - 1.0)
   
   `Math.log` is specified only to within 1 ulp and HotSpot may use a platform 
intrinsic, so these are
   permitted to differ across JVMs even though they do not on the tested one.
   
   ## Part 2 — The fix
   
   - `HllUnion.checkRebuildCurMinNumKxQ` 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.
   - `AbstractHllArray.hipAndKxQIncrementalUpdate` does not add to `hipAccum` 
when the host is
     out-of-order, matching the existing `putOutOfOrder(true)` zeroing.
   - `HllArray.toCompactByteArray()` and `DirectHllArray.toCompactByteArray()` 
set the compact flag,
     as `Hll4Array` and `DirectHll4Array` already do. Both operate on a copy, 
so a wrapped segment is
     never modified.
   - `HllUtil` pins the two RSE constants to literals — the values 
`Double.toString` prints for the
     computed form, so this is numerically a no-op here and removes the last 
place either
     implementation computes a shared constant at runtime.
   
   ## Compatibility
   
   **Serialized bytes change.** Union results carry different `curMin`, 
`numAtCurMin` and `hipAccum`;
   HLL_6 and HLL_8 compact images differ by the flag bit. Reading is unaffected 
— images from every
   earlier version still deserialize and yield identical estimates and bounds, 
because both readers of
   `numAtCurMin` branch on `curMin == 0` and `hipAccum` is not read once 
out-of-order is set.
   
   **One behavioural change beyond the bytes.** A compact image is treated as 
immutable:
   `HllSketch.wrap` reports `isCompact()`, and `writableWrap` refuses it. 
Because HLL_6 and HLL_8
   compact images now carry the flag, **`writableWrap` will reject an image 
produced by
   `toCompactByteArray()` for those types**, raising 
`SketchesArgumentException` where it previously
   succeeded. Callers who round-trip through `toCompactByteArray()` and then 
`writableWrap` should use
   `toUpdatableByteArray()`. This is the immutability contract applying 
uniformly rather than only to
   HLL_4, but it is a real change for existing callers.
   
   `HllSketchTest.checkCompactFlag` is updated accordingly: all five modes now 
read "follows the
   toByteArray request".
   
   ## Part 3 — Tests
   
   New `HllKxqRebuildTest` — 5 cases:
   
   - a union result is byte-identical across all six permutations of three 
inputs, one of which stays
     in SET mode;
   - reading an estimate mid-stream does not change a later `getResult()` 
image, at three lgMaxK;
   - a union result's stored `curMin`/`numAtCurMin` match a recount over its 
own registers;
   - `hipAccum` is 0 in any out-of-order image, independent of how many updates 
followed the merge;
   - the RSE constants match `sqrt(log 2)` and `sqrt(3 log 2 - 1)`, and 
`getRelErr` matches the closed
     form at lgK 13/16/21 for 1..3 standard deviations.
   
   **Four of the five fail without the source change.** The fifth is the 
constants test, which already
   held because Java computed them correctly; it is a guard against 
re-rounding, not a regression.
   
   Full HLL suite: 119 tests pass.
   
   ## Cross-language result
   
   With the two companion datasketches-cpp PRs, over 1041 records — lgK 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