Repository navigation
Replies: 3 comments
|
Thank you, Tison, this is an excellent and thoughtful summary of a very important issue! There are a number of compatibility cases that need to be handled explicitly: Hashing Floating Point:
Handling NULL and empty objects
String encoding Possible next steps:
However, in the past the changes we faced were simple, like the offset of the Flags byte. We have not had to deal with this situation of incompatible hash assumptions. I'm not sure there is a graceful way forward. Certainly we should change the SerVer, for detection purposes, but then what, throw an exception telling the user to revert to an earlier code version? I don't know the answer. Questions:
|
|
A follow-up on my comment above. The Count-Min version 2 spec, now in apache/datasketches-tck#21 · docs: add the Count-Min…, has placeholders for these rules in section 4: 4.4: null and empty items; Each placeholder states what Count-Min version 2 does today. For example, it ignores empty strings, empty byte arrays and nulls, and it doesn’t define floating-point items for this sketch. Each one points here for the shared policy. Once we agree on that policy, I’d suggest specifying it once in the TCK, for example in a docs/input-encoding.md, and having each family’s format spec refer to it. And to answer my own question about “empty byte sequences”: I see you defined it in section 2 of your post. It means a zero-length array (new byte[0], or a non-null C++ pointer with length zero), not an array of zeros. Thanks! |
|
Thanks @leerho! I agree with defining the shared hashed-input rules once in I suggest defining the target contract and compiling an inventory of current implementations at pinned revisions first, then working out legacy-image migration for each affected family.
Conformance should be based on an implementation's documented cross-language usage contract. For example, Rust's generic The inventory should cover Java, C++, Go, Rust, and Python by family, overload, and operation. For each case, it should record both raw API behavior and any caller-side steps required by the contract. Once the target contract and implementation differences are clear, perhaps @leerho you could help assess the compatibility options for each affected family? That includes where a If this sounds good, I can start drafting |
Uh oh!
There was an error while loading. Please reload this page.
While documenting cross-language input handling in apache/datasketches-rust#340, I found two inconsistencies in the Java and C++ implementations: empty byte inputs and signed floating-point zero. A Bloom filter written in Java can be read successfully in C++, but querying an item that was inserted in Java can return
false. As in the Count-Min discussion, it would help to agree on the input contract and compatibility implications before changing implementations.1. What happens
The reproduction below writes two separate Bloom filters in Java, each containing one item, then reads them in C++. Both use 1,024 bits, three hashes, and seed 9001.
byte[]truefalse-0.0truefalseThese results were reproduced against Java
ef440a8and C++c22f2b1, using JDK 25 and a C++17 compiler on little-endian hardware. The scope here is these source revisions; this is not a release-by-release or all-language audit.2. Empty byte inputs
Here, an empty byte input means Java
new byte[0]or a non-null C++ pointer with length zero. It does not mean a null pointer or an empty sketch.The corresponding string update APIs skip empty strings in both languages. Thus an empty string and its zero-length byte encoding can also behave differently within one implementation.
For Bloom, C++ returns
falsefor a zero-length byte query before hashing or inspecting the filter. Java performs the query normally. This also applies to queries combined with insertion. The serialized bits cannot resolve a query that returns before looking at them.The Count-Min V2 proposal already specifies ignoring empty strings and byte arrays and returning zero for queries. That is a useful explicit contract for V2; the legacy V1 row above describes the checked implementation, not the V2 proposal.
3. Signed zero
Java's
Double.doubleToLongBitscanonicalizes NaNs but preserves the sign of zero:The Java Bloom and XOR floating-point APIs pass these bits directly to xxHash64. Java HLL, CPC, Theta, and Tuple first normalize zero, effectively doing
value == 0.0 ? 0.0 : value, then callDouble.doubleToLongBits. C++ HLL, CPC, Theta, Tuple, and Bloom also normalize signed zero.+0.0+0.0+0.0Sources: Java Bloom, Java XOR, Java HLL, and C++ Bloom.
In the Bloom reproduction, Java sets the bits for
-0.0. C++ reads the image, converts a-0.0query to+0.0, and probes different positions. This is a mismatch in the bytes passed to the hash, not in xxHash64 itself. XOR demonstrates the same Java signed-zero convention, but is not a Java/C++ comparison because there is no C++ XOR implementation at the checked revision. Count-Min V2 does not define floating-point inputs.4. Minimal reproduction
Save this as
Repro.java. It writes the two Bloom images and also shows Java XOR's signed-zero behavior.Save this as
repro.cpp. It reads the exact images written by Java.Set
JAVA_REPOandCPP_REPOto checkouts of the revisions above. From a separate directory containing the two source files, run:Observed output:
The
falseresults for the empty-byte and-0.0queries in C++ are for items that were actually inserted in Java. Deserialization succeeds in both cases.5. Compatibility and possible next steps
I suggest agreeing on the input contract before adapting one implementation to another's current behavior:
-0.0and+0.0represent one item or two.-0.0still contain the old hash positions. Simply changing the update/query code would not migrate those images. Conversely, changing a reader's empty-input policy changes what it can query in existing images. We should decide whether explicit legacy handling, a format/version distinction, or a documented limitation is needed.The Rust PR linked above documents input strategies and empty-input handling; it does not change runtime hashing behavior. Rust's existing
canonical_floatcontinues to normalize both signed zero and NaNs.6. Questions for the community
All reactions