hash4j
repository·main·Indexed 41 days ago
https://github.com/dynatrace-oss/hash4jA high-performance Java library by Dynatrace providing non-cryptographic hash algorithms and data structures. It features implementations of Murmur3, Wyhash, Komihash, FarmHash, XXHash, and others, alongside tools for similarity estimation (MinHash, SimHash), approximate distinct counting (HyperLogLog, UltraLogLog), consistent hashing (JumpHash, JumpBackHash), and fast file hashing via Imohash. The library is designed for efficiency, supporting streaming hashing and allocation-free updates to minimize memory overhead.
What's inside hash4j
- hash4j is a Java library by Dynatrace that provides various non-cryptographic hash algorithms and data structures built on high-quality hash functions. It is designed for performance and efficiency, supporting streaming hashing of Java objects to minimize memory allocations and constant memory footprints.
How similarity hashing works
mainSimilarity hashing algorithms compute hash signatures for sets, allowing you to estimate set similarity (like Jaccard similarity) without storing the original sets.
Supported algorithms:
- MinHash
- SuperMinHash
- SimHash
- FastSimHash (a bit-hack optimized SimHash implementation)
To use it, you create a
SimilarityHashPolicy(e.g., usingsuperMinHash), create aSimilarityHasher, and compute signatures using anElementHashProviderthat maps elements to long hashes.ToLongFunction<String> stringHashFunc = s -> Hashing.komihash5_0().hashCharsToLong(s); Set<String> setA = IntStream.range(0, 90000).mapToObj(Integer::toString).collect(toSet()); Set<String> setB = IntStream.range(0, 100000).mapToObj(Integer::toString).collect(toSet()); int numberOfComponents = 1024; int bitsPerComponent = 1; SimilarityHashPolicy policy = SimilarityHashing.superMinHash(numberOfComponents, bitsPerComponent); SimilarityHasher simHasher = policy.createHasher(); byte[] signatureA = simHasher.compute(ElementHashProvider.ofCollection(setA, stringHashFunc)); byte[] signatuerB = simHasher.compute(ElementHashProvider.ofCollection(setB, stringHashFunc)); double fractionOfEqualComponents = policy.getFractionOfEqualComponents(signatureA, signatuerB); // Estimate Jaccard similarity double estimatedJaccardSimilarity = (fractionOfEqualComponents - Math.pow(2., -bitsPerComponent)) / (1. - Math.pow(2., -bitsPerComponent));Understand HyperLogLog estimation error and precision
mainWhen using HyperLogLog for approximate distinct counting, the accuracy and memory usage are determined by the precision parameter $p$.
Memory Usage
The memory required for a HyperLogLog sketch is calculated as:
0.75 * mbytes, wherem = 2^p(the number of registers).Expected Relative Standard Error
The expected relative standard error depends on the estimator used. For $p \geq 6$ and large distinct counts, the error is approximately:
- Default estimator: $\frac{1.039}{\sqrt{m}}$
- Maximum-likelihood (ML) estimator: $\frac{1.037}{\sqrt{m}}$
- Martingale estimator: $\frac{0.833}{\sqrt{m}}$
Accuracy Notes
- Small Counts: The error is significantly smaller when the distinct count is in the order of $m$ or smaller.
- Bias: The bias is much smaller than the root-mean-square error (rmse) and can generally be neglected.
- Precision: Higher values of $p$ increase the number of registers $m$, which reduces the relative error but increases memory consumption.
Approximate distinct counting with HyperLogLog and UltraLogLog
mainFor counting distinct elements in large data streams with minimal memory, hash4j provides two main algorithms:
- HyperLogLog: Uses 6-bit registers. It is highly space-efficient and supports various estimators (default, maximum-likelihood, or martingale for non-distributed streams).
- UltraLogLog: Uses 8-bit registers for faster random access and updates. It offers a significant reduction in storage factor compared to HyperLogLog.
Both algorithms feature:
- Constant-time add operations
- Allocation-free updates
- Idempotency
- Mergeability (even with different precision parameters)
- Order-independent final state
Hasher64 hasher = Hashing.komihash5_0(); // Create an UltraLogLog sketch with precision 12 UltraLogLog sketch = UltraLogLog.create(12); sketch.add(hasher.hashCharsToLong("foo")); sketch.add(hasher.hashCharsToLong("bar")); sketch.add(hasher.hashCharsToLong("foo")); double distinctCountEstimate = sketch.getDistinctCountEstimate(); // close to 2How consistent hashing works in hash4j
mainConsistent hashing allows for the distributed agreement on assigning keys to buckets. Unlike a naive modulo operation (
hash % numBuckets), which causes most keys to be reassigned when the number of buckets changes, consistent hashing minimizes reassignments.The library provides two main types of hashers:
- ConsistentBucketHasher: Stateless mappings that require only the hash of the key and the number of buckets. These are suitable when buckets are added or removed only at the end of the list (LIFO/FIFO style).
- ConsistentBucketSetHasher: Stateful mappings used when buckets are added or removed in an arbitrary order. To ensure consistent mapping across a distributed environment, the state must be shared (e.g., by exporting/importing the state as a byte array).
Understand UltraLogLog estimation error and precision
mainWhen using an UltraLogLog sketch for approximate distinct counting, the estimation error is determined by the precision parameter $p$. The sketch uses $m = 2^p$ registers (bytes) of state.
The expected relative standard error depends on the estimator used:
- Default estimator: $\approx \frac{0.782}{\sqrt{m}}$
- Maximum-likelihood (ML) estimator: $\approx \frac{0.761}{\sqrt{m}}$
- Martingale estimator: $\approx \frac{0.658}{\sqrt{m}}$
Key considerations for accuracy:
- These approximations are reliable for $p \geq 6$ and large distinct counts.
- Error is significantly lower when the distinct count is equal to or smaller than $m$.
- Bias is generally much smaller than the root-mean-square error (rmse) and can be neglected.
- For empirical error rates across different precision levels, refer to the simulation charts in the repository documentation.
Hash Java objects using streaming or funnels
mainThe
Hashinginterface allows you to hash Java objects without first mapping them to byte arrays, which minimizes memory allocations. You can use three main approaches:- Hash Stream: Pass data directly into a hash stream (e.g.,
putInt,putLong,putString). - Hash Funnel: Define a
HashFunnelthat describes how to map an object to the hash sink. - Reusable Hash Stream: For allocation-free performance, reuse a pre-allocated
HashStreaminstance by calling.reset().
class TestClass { int a = 42; long b = 1234567890L; String c = "Hello world!"; } TestClass obj = new TestClass(); var hasher = Hashing.komihash5_0(); // variant 1: hash object by passing data into a hash stream long hash1 = hasher.hashStream().putInt(obj.a).putLong(obj.b).putString(obj.c).getAsLong(); // variant 2: hash object by defining a funnel HashFunnel<TestClass> funnel = (o, sink) -> sink.putInt(o.a).putLong(o.b).putString(o.c); long hash2 = hasher.hashToLong(obj, funnel); // variant 3: allocation-free by reusing a pre-allocated hash stream instance var hashStream = Hashing.komihash5_0().hashStream(); long hash3 = hashStream.reset().putInt(obj.a).putLong(obj.b).putString(obj.c).getAsLong(); // variant 4: allocation-free and using a funnel long hash4 = hashStream.resetAndHashToLong(obj, funnel);- Hash Stream: Pass data directly into a hash stream (e.g.,
Convert and downsize distinct counting sketches
mainYou can manipulate the precision and type of distinct counting sketches:
- Downsizing: Both HyperLogLog and UltraLogLog sketches can be reduced to a smaller precision parameter
pusingsketch.downsize(p). - Conversion: UltraLogLog sketches can be converted into HyperLogLog sketches using
HyperLogLog.create(ultraLogLog).
- Downsizing: Both HyperLogLog and UltraLogLog sketches can be reduced to a smaller precision parameter
Set up the Python environment for development
mainThe project contains Python code. It is recommended to use a virtual environment in a
.venvdirectory. To install the required dependencies with hash-pinned security, run:python -m pip install --only-binary :all: --require-hashes -r requirements.txtRegenerate locked Python dependencies
mainIf you modify
requirements.inor need to update transitive dependencies, usepip-toolsto regenerate the hash-pinnedrequirements.txtfile:python -m pip install pip-tools pip-compile --generate-hashes --allow-unsafe --no-strip-extras --output-file requirements.txt requirements.inUse ImprovedConsistentWeightedSampling for weighted consistent hashing
mainThe
ImprovedConsistentWeightedSamplingclass implements theConsistentBucketHasherinterface using a simplified version of the algorithm described by Sergey Ioffe (2010). It is designed for consistent hashing where buckets can be sampled based on weights, providing a way to map a hash value to a specific bucket index in a way that minimizes reshuffling when the number of buckets changes.Note: This class is package-private and is intended to be used through the public API of the
consistentpackage (e.g., via a factory or a higher-level consistent hashing manager).Perform fast file hashing with Imohash
mainThe library provides an implementation of Imohash for fast file hashing. It works by hashing only the beginning, middle, and end of large files, making it efficient for file indexing or finding identical files.
Warning: This is not a cryptographic hashing algorithm and should not be used for verifying file integrity.
// create some file in the given path File file = path.resolve("test.txt").toFile(); try (FileWriter fileWriter = new FileWriter(file, StandardCharsets.UTF_8)) { fileWriter.write("this is the file content"); } // use ImoHash to hash that file HashValue128 hash = FileHashing.imohash1_0_2().hashFileTo128Bits(file); // returns 0xd317f2dad6ea7ae56ff7fdb517e33918