【问题标题】:compression on java nio direct buffersjava nio直接缓冲区上的压缩
【发布时间】:2012-01-07 00:44:57
【问题描述】:

gzip 输入/输出流不在 Java 直接缓冲区上运行。

是否有任何直接在直接缓冲区上运行的压缩算法实现?

这样就没有将直接缓冲区复制到 java 字节数组进行压缩的开销。

【问题讨论】:

  • 没有开销的压缩是不可能的。根据定义,直接缓冲区是“特定原始类型的固定数量数据的容器”。诸如压缩或加密之类的转换必须在缓冲区之外完成。
  • 我明白。我只想进行压缩,而不会增加首先将整个直接缓冲区数组复制到 java 字节数组的惩罚
  • GZIPInputStream 不会创建副本 - 它直接从文件中流出(基于检查源)。所以我想它可能比创建自己的直接缓冲区并将文件映射到它更快。如果你真的想使用直接缓冲区,你可以编写你自己的 InputStream 从你的缓冲区流...
  • GZIP 压缩比仅复制数据要慢得多,它不太可能产生太大影响。
  • russell:我的直接缓冲区不是从文件创建的。我正在创建我的代码以避免 gc

标签: java compression gzip nio


【解决方案1】:

我并不是要贬低你的问题,但是这真的是你程序中一个很好的优化点吗?您是否使用分析器验证您确实有问题?如上所述,您的问题意味着您没有进行任何研究,而只是猜测分配字节 [] 会出现性能或内存问题。由于此线程中的所有答案都可能是某种黑客行为,因此您应该在修复之前真正确认您确实有问题。

回到问题,如果您想在 ByteBuffer 上“就地”压缩数据,答案是否定的,Java 没有内置的功能。

如果您按以下方式分配缓冲区:

byte[] bytes = getMyData();
ByteBuffer buf = ByteBuffer.wrap(bytes);

您可以按照上一个答案的建议通过 ByteBufferInputStream 过滤您的 byte[]。

【讨论】:

  • 我接受这个作为答案,但仍在等待提供解决方案的解决方案,比如以在字节缓冲区上使用 jni 进行操作的库的形式。
  • 我对这个问题很好奇,因为我想找到一种方法将文件夹转换为仅名称的 zip 文件,以便快速删除大文件夹。
  • 避免复制数据几乎总是能显着提升性能。然而,已经在直接缓冲区中的数据不能在不被复制的情况下被压缩,除非由操作系统本身完成。
【解决方案2】:

哇老问题,但今天偶然发现。

可能像 zip4j 这样的一些库可以处理这个问题,但你可以在 Java 11 之后完成这项工作而无需外部依赖:

如果您只对压缩数据感兴趣,您可以这样做:

void compress(ByteBuffer src, ByteBuffer dst) {
    var def = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    try {
        def.setInput(src);
        def.finish();
        def.deflate(dst, Deflater.SYNC_FLUSH);

        if (src.hasRemaining()) {
            throw new RuntimeException("dst too small");
        }
    } finally {
        def.end();
    }
}

src 和 dst 都会改变位置,所以你可能需要在 compress 返回后翻转它们。

为了恢复压缩数据:

void decompress(ByteBuffer src, ByteBuffer dst) throws DataFormatException {
    var inf = new Inflater(true);
    try {
        inf.setInput(src);
        inf.inflate(dst);

        if (src.hasRemaining()) {
            throw new RuntimeException("dst too small");
        }

    } finally {
        inf.end();
    }
}

请注意,这两种方法都希望(解)压缩在一次通过中发生,但是,我们可以使用稍微修改的版本来流式传输它:

void compress(ByteBuffer src, ByteBuffer dst, Consumer<ByteBuffer> sink) {
    var def = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
    try {
        def.setInput(src);
        def.finish();
        int cmp;
        do {
            cmp = def.deflate(dst, Deflater.SYNC_FLUSH);
            if (cmp > 0) {
                sink.accept(dst.flip());
                dst.clear();
            }
        } while (cmp > 0);
    } finally {
        def.end();
    }
}

void decompress(ByteBuffer src, ByteBuffer dst, Consumer<ByteBuffer> sink) throws DataFormatException {
    var inf = new Inflater(true);
    try {
        inf.setInput(src);
        int dec;
        do {
            dec = inf.inflate(dst);

            if (dec > 0) {
                sink.accept(dst.flip());
                dst.clear();
            }

        } while (dec > 0);
    } finally {
        inf.end();
    }
}

例子:

void compressLargeFile() throws IOException {
    var in = FileChannel.open(Paths.get("large"));
    var temp = ByteBuffer.allocateDirect(1024 * 1024);
    var out = FileChannel.open(Paths.get("large.zip"));

    var start = 0;
    var rem = ch.size();
    while (rem > 0) {
        var mapped=Math.min(16*1024*1024, rem);
        var src = in.map(MapMode.READ_ONLY, start, mapped);

        compress(src, temp, (bb) -> {
            try {
                out.write(bb);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
        
        rem-=mapped;
    }
}

如果您想要完全符合 zip 标准的数据:

void zip(ByteBuffer src, ByteBuffer dst) {
    var u = src.remaining();
    var crc = new CRC32();
    crc.update(src.duplicate());
    writeHeader(dst);

    compress(src, dst);

    writeTrailer(crc, u, dst);
}

地点:

void writeHeader(ByteBuffer dst) {
    var header = new byte[] { (byte) 0x8b1f, (byte) (0x8b1f >> 8), Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 };
    dst.put(header);
}

还有:

void writeTrailer(CRC32 crc, int uncompressed, ByteBuffer dst) {
    if (dst.order() == ByteOrder.LITTLE_ENDIAN) {
        dst.putInt((int) crc.getValue());
        dst.putInt(uncompressed);
    } else {
        dst.putInt(Integer.reverseBytes((int) crc.getValue()));
        dst.putInt(Integer.reverseBytes(uncompressed));
    }

因此,zip 会产生 10+8 字节的开销。

为了将一个直接缓冲区解压缩到另一个缓冲区中,您可以将 src 缓冲区包装到一个 InputStream 中:

class ByteBufferInputStream extends InputStream {

    final ByteBuffer bb;

    public ByteBufferInputStream(ByteBuffer bb) {
        this.bb = bb;
    }

    @Override
    public int available() throws IOException {
        return bb.remaining();
    }

    @Override
    public int read() throws IOException {
        return bb.hasRemaining() ? bb.get() & 0xFF : -1;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        var rem = bb.remaining();

        if (rem == 0) {
            return -1;
        }

        len = Math.min(rem, len);

        bb.get(b, off, len);

        return len;
    }

    @Override
    public long skip(long n) throws IOException {
        var rem = bb.remaining();

        if (n > rem) {
            bb.position(bb.limit());
            n = rem;
        } else {
            bb.position((int) (bb.position() + n));
        }

        return n;
    }
}

并使用:

void unzip(ByteBuffer src, ByteBuffer dst) throws IOException {
    try (var is = new ByteBufferInputStream(src); var gis = new GZIPInputStream(is)) {
        var tmp = new byte[1024];

        var r = gis.read(tmp);

        if (r > 0) {
            do {
                dst.put(tmp, 0, r);
                r = gis.read(tmp);
            } while (r > 0);
        }

    }
}

当然,这并不酷,因为我们正在将数据复制到一个临时数组,但尽管如此,它还是一种往返检查,证明基于 nio 的 zip 编码写入的有效数据可以从基于标准 io 的消费者。

所以,如果我们忽略 crc 一致性检查,我们可以直接删除页眉/页脚:

void unzipNoCheck(ByteBuffer src, ByteBuffer dst) throws DataFormatException {
    src.position(src.position() + 10).limit(src.limit() - 8);

    decompress(src, dst);
}

【讨论】:

    【解决方案3】:

    如果您使用的是 ByteBuffers,您可以使用一些简单的 Input/OutputStream 包装器,例如:

    public class ByteBufferInputStream extends InputStream {
    
        private ByteBuffer buffer = null;
    
        public ByteBufferInputStream( ByteBuffer b) {
            this.buffer = b;
        }
    
        @Override
        public int read() throws IOException {
            return (buffer.get() & 0xFF);
        }
    }
    
    public class ByteBufferOutputStream extends OutputStream {
    
        private ByteBuffer buffer = null;
    
        public ByteBufferOutputStream( ByteBuffer b) {
            this.buffer = b;
        }
    
        @Override
        public void write(int b) throws IOException {
            buffer.put( (byte)(b & 0xFF) );
        }
    
    }
    

    测试:

    ByteBuffer buffer = ByteBuffer.allocate( 1000 );
    ByteBufferOutputStream bufferOutput = new ByteBufferOutputStream( buffer );
    GZIPOutputStream output = new GZIPOutputStream( bufferOutput );
    output.write("stackexchange".getBytes());
    output.close();
    
    buffer.position( 0 );
    
    byte[] result = new byte[ 1000 ];
    
    ByteBufferInputStream bufferInput = new ByteBufferInputStream( buffer );
    GZIPInputStream input = new GZIPInputStream( bufferInput );
    input.read( result );
    
    System.out.println( new String(result));
    

    【讨论】:

    • 即使将字节缓冲区包装到流中也无济于事,因为它是在内部复制的(有时是两次),有点违背了字节缓冲区的目的
    • 抱歉,我不明白,该副本何时会出现?我仔细检查了 InputStream、OutputStream 甚至 GZIP 类的代码,但找不到任何副本。
    • 它是这样工作的,检查 InflatedInputStream 并且本机 impl 必须复制(或 pin,取决于 JVM/GC)byte[] 以将其传递给 zlib
    猜你喜欢
    • 2023-03-06
    • 1970-01-01
    • 1970-01-01
    • 2014-03-16
    • 2016-08-02
    • 1970-01-01
    • 1970-01-01
    • 2013-12-01
    • 1970-01-01
    相关资源
    最近更新 更多