如何在 Java 中加速读写 base64 编码的 gzip 大文件

How to speed up read write base64 encoded gzipped large files in Java

任务是compress/decompress非常大的数据>2G,单个String或者ByteArray无法容纳。我的解决方案是将 compressed/decompressed 数据块逐块写入文件。它有效,但速度不够快。

压缩: 纯文本文件 -> gzip -> base64 编码 -> 压缩文件
解压:压缩文件->base64解码->gunzip->纯文本文件

笔记本电脑测试结果,16G内存。

Created compressed file, takes 571346 millis
Created decompressed file, takes 378441 millis

代码块

public static void compress(final InputStream inputStream, final Path outputFile) throws IOException {
    try (final OutputStream outputStream = new FileOutputStream(outputFile.toString());
        final OutputStream base64Output = Base64.getEncoder().wrap(outputStream);
        final GzipCompressorOutputStream gzipOutput = new GzipCompressorOutputStream(base64Output);
        final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

      reader.lines().forEach(line -> {
        try {
          gzipOutput.write(line.getBytes());
          gzipOutput.write(System.getProperty("line.separator").getBytes());
        } catch (final IOException e) {
          e.printStackTrace();
        }
      });
    }
  }

public static void decompress(final InputStream inputStream, final Path outputFile) throws IOException {
  try (final OutputStream outputStream = new FileOutputStream(outputFile.toString());
      final GzipCompressorInputStream gzipStream = new GzipCompressorInputStream(Base64.getDecoder().wrap(inputStream));
      final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipStream))) {

    reader.lines().forEach(line -> {
      try {
        outputStream.write(line.getBytes());
        outputStream.write(System.getProperty("line.separator").getBytes());
      } catch (final IOException e) {
        e.printStackTrace();
      }
    });
  }
}

此外,我尝试在将数据发送到文件时进行批量写入,但没有看到太大的改进。

# batch write
public static void compress(final InputStream inputStream, final Path outputFile) throws IOException {
  try (final OutputStream outputStream = new FileOutputStream(outputFile.toString());
      final OutputStream base64Output = Base64.getEncoder().wrap(outputStream);
      final GzipCompressorOutputStream gzipOutput = new GzipCompressorOutputStream(base64Output);
      final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

    StringBuilder stringBuilder = new StringBuilder();
    final int chunkSize = Integer.MAX_VALUE / 1000;

    String line;
    int counter = 0;
    while((line = reader.readLine()) != null) {
      counter++;
      stringBuilder.append(line).append(System.getProperty("line.separator"));
      if(counter >= chunkSize) {
        gzipOutput.write(stringBuilder.toString().getBytes());
        counter = 0;
        stringBuilder = new StringBuilder();
      }
    }

    if (counter > 0) {
      gzipOutput.write(stringBuilder.toString().getBytes());
    }
  }
}

问题

  1. 正在寻找有关如何加快整个过程的建议
  2. 瓶颈是什么?

10/2/2019 更新

我又做了一些测试,结果显示base64编码是瓶颈。

public static void compress(final InputStream inputStream, final Path outputFile) throws IOException {
  try (final OutputStream outputStream = new FileOutputStream(outputFile.toString());
       final OutputStream base64Output = Base64.getEncoder().wrap(outputStream);
       final GzipCompressorOutputStream gzipOutput = new GzipCompressorOutputStream(base64Output)) {

    final byte[] buffer = new byte[4096];
    int n = 0;
    while (-1 != (n = inputStream.read(buffer))) {
      gzipOutput.write(buffer, 0, n);
    }
  }
}

大文件总是需要一些时间,但我看到了两个重要的机会:

  1. 如果可能,删除 Base64 步骤。它使文件更大,更大的数据花费更多的时间 read/write。还有base64转换本身的成本。
  2. 不要使用基于 line 的 IO。实际上根本不使用字符串。搜索换行符并在纯字节和 string 对象之间转换数据会花费时间,并且在这里没有用:工作已撤消并且数据的形式为 从来没有真正使用过,它只是一种随意分割数据的方式。

对于更快的流到流复制,您可以使用例如 IOUtils.copy(in, out)(它也在 Apache Commons 中,看起来您已经在使用它),或者您自己实施类似的策略:将数据块读入 byte[](几 KB,不是很小的东西),然后将其写出到输出流,直到输入全部被读取。

首先:永远不要默认字符集,因为它不可移植。

String s = ...;
byte[] b = ...;
b = s.getBytes(StandardCharsets.UTF_8);
s = new String(b, StandardCharsets.UTF_8);

对于文本压缩,不涉及 Reader,因为它将给定某些字符集的字节转换为字符串(包含 Unicode),然后再次转换回来。此外,字符串的 char 需要 2 个字节 (UTF-16),而基本 ASCII 符号需要 1 个字节。

Base64 将二进制转换为 64 个 ASCII 符号的字母表,需要 space 的 4/3。当数据必须以 XML 等格式打包传输时,不要这样做。

可以(解)压缩大文件。

final int BUFFER_SIZE = 1024 * 64;
Path textFile = Paths.get(".... .txt");
Path gzFile = textFile.resolveSibling(textFile.getFileName().toString() + ".gz");

try (OutputStream out = new GzipOutputStream(Files.newOutputStream(gzFile), BUFFER_SIZE))) {
    Files.copy(textFile, out);
}

try (InputStream in = new GzipInputStream(Files.newInputStream(gzFile), BUFFER_SIZE))) {
    Files.copy(in, textFile);
}

经常忽略可选参数 BUFFER_SIZE,这可能会降低性能。

copy 可以有额外的参数来处理文件冲突。