Aircompressor

repository·master·Indexed 20 days ago

https://github.com/airlift/aircompressor

A high-performance Java compression library supporting Zstd, LZ4, Snappy, LZO, and Deflate. It provides both pure Java (via sun.misc.Unsafe) and native implementations (via java.lang.foreign) for byte arrays and MemorySegments. The library also includes XXHash3, XXHash64, and XXHash32 hashing, as well as BZip2 and Hadoop-compatible LZO/LZ4 stream implementations. Requires Java 22+, a little endian platform, and the sun.misc.Unsafe interface.

Tokens
3.4K
Snippets
9
Records
14
Agent score
56%

What's inside airlift-aircompressor

  1. Choose a compression algorithm

    master

    Aircompressor supports several algorithms depending on your performance and resource requirements:

    • Zstandard (Zstd): Recommended for most use cases. Provides superior compression and performance compared to zlib. Use ZstdNativeCompressor/Decompressor (native) or ZstdJavaCompressor/Decompressor (Java).
    • LZ4: Extremely fast; ideal for high-performance requirements. Use Lz4NativeCompressor/Decompressor or Lz4JavaCompressor/Decompressor. Supports the LZ4 frame format via Lz4FrameNativeCompressor or Lz4FrameJavaCompressor.
    • Snappy: Good for extremely resource-limited environments due to guaranteed memory usage. Use SnappyNativeCompressor/Decompressor or SnappyJavaCompressor/Decompressor.
    • LZO: Provided for compatibility only. Use LzoCompressor/Decompressor (Java implementation only).
    • Deflate: Provided for compatibility with gzip/zlib. Use DeflateCompressor/Decompressor.
  2. Configure native library loading

    master

    You can control how and where native libraries are loaded using system properties:

    • aircompressor.tmpdir: Sets the temporary directory used to unpack and load native libraries. Defaults to java.io.tmpdir. Use this if your default temp directory is mounted as noexec.
    • io.aircompress.v3.disable-native: Set this to disable native library loading entirely.
  3. Use XXHash3 for fast hashing

    master

    Recommended for high-performance hashing. XXHash3 is available only as a native implementation via XxHash3Native. It supports 64-bit and 128-bit outputs. Note that the 128-bit variant has a small constant overhead (~12ns) when pulling results back into Java via FFM, which is negligible for inputs > 8KB.

    // One-shot hashing (64-bit)
    long hash = XxHash3Native.hash(data);
    
    // One-shot hashing (128-bit)
    XxHash128 hash = XxHash3Native.hash128(data);
    
    // Streaming hashing (64-bit)
    try (XxHash3Hasher hasher = XxHash3Native.newHasher()) {
        hasher.update(chunk1);
        hasher.update(chunk2);
        long hash = hasher.digest();
    }
    
    // Streaming hashing (128-bit)
    try (XxHash3Hasher128 hasher = XxHash3Native.newHasher128()) {
        hasher.update(chunk1);
        hasher.update(chunk2);
        XxHash128 hash = hasher.digest();
    }
  4. Compress and decompress MemorySegments

    master

    For high-performance memory access using java.lang.foreign.MemorySegment, use the Compressor and Decompressor APIs. This approach avoids the overhead of copying data to byte arrays. Ensure you use an Arena to manage the lifecycle of the allocated segments.

    Arena arena = ...
    MemorySegment data = ...
    
    Compressor compressor = new Lz4JavaCompressor();
    MemorySegment compressed = arena.allocate(compressor.maxCompressedLength(toIntExact(data.byteSize())));
    int compressedSize = compressor.compress(data, compressed);
    compressed = compressed.asSlice(0, compressedSize);
    
    Decompressor decompressor = new Lz4JavaDecompressor();
    MemorySegment uncompressed = arena.allocate(data.byteSize());
    int uncompressedSize = decompressor.decompress(compressed, uncompressed);
    uncompressed = uncompressed.asSlice(0, uncompressedSize);
  5. Compress and decompress byte arrays

    master

    Use the Compressor and Decompressor classes for block compression of byte[] data. You must first call compressor.maxCompressedLength(data.length) to determine the required size for the destination buffer to avoid overflows.

    byte[] data = ...
    
    Compressor compressor = new Lz4JavaCompressor();
    byte[] compressed = new byte[compressor.maxCompressedLength(data.length)];
    int compressedSize = compressor.compress(data, 0, data.length, compressed, 0, compressed.length);
    
    Decompressor decompressor = new Lz4JavaDecompressor();
    byte[] uncompressed = new byte[data.length];
    int uncompressedSize = decompressor.decompress(compressed, 0, compressedSize, uncompressed, 0, uncompressed.length);
  6. Use XXHash64 or XXHash32

    master

    For 64-bit or 32-bit hashing, use the XxHash64Hasher or XxHash32Hasher interfaces. These interfaces provide static methods that automatically select the best available implementation (Native or Java).

    // XXHash64 One-shot
    long hash = XxHash64Hasher.hash(data);
    long hash = XxHash64Hasher.hash(data, seed);
    
    // XXHash64 Streaming
    try (XxHash64Hasher hasher = XxHash64Hasher.create()) {
        hasher.update(chunk1);
        long hash = hasher.digest();
    }
    
    // XXHash32 One-shot
    int hash = XxHash32Hasher.hash(data);
    int hash = XxHash32Hasher.hash(data, seed);
    
    // XXHash32 Streaming
    try (XxHash32Hasher hasher = XxHash32Hasher.create()) {
        hasher.update(chunk1);
        int hash = hasher.digest();
    }
  7. Get processed byte count from CBZip2InputStream

    master

    The getProcessedByteCount() method returns the number of bytes processed from the compressed stream.

    Note: This statistic is only updated on block boundaries and is only valid when the stream is initiated in BYBLOCK reading mode. It does not provide a continuous update of the position during the middle of a block.

    long bytesProcessed = cbZip2InputStream.getProcessedByteCount();
  8. Use CBZip2OutputStream for BZip2 compression

    master

    CBZip2OutputStream is an OutputStream that compresses data into the BZip2 format (excluding the file header characters).

    Important Requirements:

    • Magic Bytes: The caller is responsible for writing the two BZip2 magic bytes "BZ" to the destination stream before initializing this class.
    • Memory Management: Compression requires significant memory. You should call .close() or .finish() as soon as possible to release allocated memory.
    • Thread Safety: Instances of this class are not thread-safe.

    Block Size and Memory Usage: You can tune the blockSize (expressed in 100k units, from 1 to 9) to balance compression ratio, speed, and memory usage. A lower block size reduces memory usage and can increase speed but may lower the compression ratio.

    Memory Estimation Formulas:

    • Compression memory usage: 400k + (9 * blocksize)
    • Decompression memory usage: 65k + (5 * blocksize)
    BlocksizeCompression MemoryDecompression Memory
    100k1300k565k
    200k2200k1065k
    300k3100k1565k
    400k4000k2065k
    500k4900k2565k
    600k5800k3065k
    700k6700k3565k
    800k7600k4065k
    900k8500k4565k
    // Example setup (pseudo-code for context)
    OutputStream fileOut = new FileOutputStream("data.bz2");
    fileOut.write('B');
    fileOut.write('Z');
    
    try (CBZip2OutputStream bzipOut = new CBZip2OutputStream(fileOut)) {
        bzipOut.write(myData);
    }
  9. Create an Lz4HadoopInputStream

    master

    Use Lz4HadoopInputStream to create a Hadoop-compatible input stream for reading LZ4 compressed data. This class extends HadoopInputStream and is designed to work within Hadoop-style data processing pipelines.

    To instantiate it, you must provide:

    1. An Lz4Decompressor instance.
    2. An existing InputStream containing the compressed data.
    3. A maxUncompressedLength integer, which defines the size of the internal buffer used for decompression.
    Lz4HadoopInputStream inputStream = new Lz4HadoopInputStream(decompressor, compressedInputStream, maxUncompressedLength);
  10. Use LzopHadoopOutputStream for Hadoop-compatible LZO compression

    master

    LzopHadoopOutputStream is a Hadoop-compatible output stream that compresses data using the LZO algorithm with the lzop file format. It is designed to be used as a wrapper around an existing OutputStream.

    Constructor

    LzopHadoopOutputStream(OutputStream out, int bufferSize)

    • out: The underlying destination OutputStream.
    • bufferSize: The size of the internal input buffer. A larger buffer can improve compression efficiency but increases memory usage.

    Lifecycle Methods

    • write(int b): Writes a single byte to the stream.
    • write(byte[] buffer, int offset, int length): Writes a portion of a byte array to the stream. This method is optimized to avoid extra copies when possible.
    • finish(): Flushes any remaining data in the input buffer by compressing and writing the final chunk.
    • flush(): Flushes the underlying OutputStream.
    • close(): Finalizes the LZO stream by calling finish(), writing a termination marker (0), and closing the underlying OutputStream.
    OutputStream out = new FileOutputStream("data.lzo");
    try (LzopHadoopOutputStream lzoOut = new LzopHadoopOutputStream(out, 65536)) {
        lzoOut.write("Hello, LZO!".getBytes());
    }
  11. Use CBZip2InputStream for BZip2 decompression

    master

    CBZip2InputStream is an InputStream that decompresses data in the BZip2 format.

    Important Usage Notes

    • Header Requirement: The constructor expects the stream to start at the first byte after the BZip2 magic bytes (Bz). You must manually skip the first two bytes of the BZip2 stream before passing the InputStream to the constructor, otherwise an exception will be thrown.
    • Memory Management: Decompression requires large amounts of memory. You should call .close() as soon as possible to force the release of allocated memory.
    • Performance: The implementation reads from the source stream using single-byte read() calls. It is highly recommended to wrap your source InputStream in a BufferedInputStream to improve performance.
    • Thread Safety: Instances of this class are not thread-safe.
    • Block-based Reading: This implementation supports a mode where it identifies BZip2 block boundaries. In this mode, it can report the position in the compressed stream at the end of each block.
    // Assuming 'compressedStream' is a BZip2 stream
    // 1. Skip the first two bytes ('B' and 'z')
    InputStream bufferedStream = new BufferedInputStream(compressedStream);
    bufferedStream.skip(2);
    
    // 2. Initialize the decompressor
    try (CBZip2InputStream bzip2Stream = new CBZip2InputStream(bufferedStream)) {
        int byteRead;
        while ((byteRead = bzip2Stream.read()) != -1) {
            // Process decompressed byte
        }
    }