hash4j

repository·main·Indexed 41 days ago

https://github.com/dynatrace-oss/hash4j

A 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.

Tokens
8.5K
Snippets
25
Records
33
Agent score
51%

What's inside hash4j

  1. Overview of hash4j

    main
    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.
  2. How similarity hashing works

    main

    Similarity 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., using superMinHash), create a SimilarityHasher, and compute signatures using an ElementHashProvider that 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));
  3. Understand HyperLogLog estimation error and precision

    main

    When 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 * m bytes, where m = 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.
  4. Approximate distinct counting with HyperLogLog and UltraLogLog

    main

    For 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 2
  5. How consistent hashing works in hash4j

    main

    Consistent 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:

    1. 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).
    2. 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).
  6. Understand UltraLogLog estimation error and precision

    main

    When 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.
  7. Hash Java objects using streaming or funnels

    main

    The Hashing interface allows you to hash Java objects without first mapping them to byte arrays, which minimizes memory allocations. You can use three main approaches:

    1. Hash Stream: Pass data directly into a hash stream (e.g., putInt, putLong, putString).
    2. Hash Funnel: Define a HashFunnel that describes how to map an object to the hash sink.
    3. Reusable Hash Stream: For allocation-free performance, reuse a pre-allocated HashStream instance 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);
  8. Convert and downsize distinct counting sketches

    main

    You can manipulate the precision and type of distinct counting sketches:

    • Downsizing: Both HyperLogLog and UltraLogLog sketches can be reduced to a smaller precision parameter p using sketch.downsize(p).
    • Conversion: UltraLogLog sketches can be converted into HyperLogLog sketches using HyperLogLog.create(ultraLogLog).
  9. Set up the Python environment for development

    main

    The project contains Python code. It is recommended to use a virtual environment in a .venv directory. To install the required dependencies with hash-pinned security, run:

    python -m pip install --only-binary :all: --require-hashes -r requirements.txt
  10. Regenerate locked Python dependencies

    main

    If you modify requirements.in or need to update transitive dependencies, use pip-tools to regenerate the hash-pinned requirements.txt file:

    python -m pip install pip-tools
    pip-compile --generate-hashes --allow-unsafe --no-strip-extras --output-file requirements.txt requirements.in
  11. Use ImprovedConsistentWeightedSampling for weighted consistent hashing

    main

    The ImprovedConsistentWeightedSampling class implements the ConsistentBucketHasher interface 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 consistent package (e.g., via a factory or a higher-level consistent hashing manager).

  12. Perform fast file hashing with Imohash

    main

    The 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