Как ускорить чтение, запись base64, закодированные сжатые большие файлы в Java - PullRequest
1 голос
/ 02 октября 2019

Задача - сжать / распаковать очень большие данные > 2G , которые не могут быть удержаны одной строкой или массивом ByteArray. Мое решение заключается в записи сжатых / распакованных данных в файл. Это работает, но не достаточно быстро.

Сжатие : текстовый файл -> 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);
    }
  }
}
  • 2.2G тестовый файл, с 21,5 миллионами строк
  • Копировать только файл: ~ 2 секунды
  • Только файл Gzip: ~ 12 секунд
  • Gzip + base64: ~ 500 секунд

Ответы [ 2 ]

2 голосов
/ 02 октября 2019

Во-первых: никогда не использовать кодировку по умолчанию, так как она не переносима.

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

Для сжатия текста не используйте Reader, поскольку он преобразует байты заданного набора символов в строку (содержащую Unicode),и снова обращение обратно. Также для символа String требуется 2 байта (UTF-16), а не 1 байт для основных символов ASCII.

Base64 преобразует двоичный код в алфавит из 64 символов ASCII, занимая 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 может иметь дополнительные параметры для обработки конфликтов файлов.

2 голосов
/ 02 октября 2019

Большие файлы всегда будут занимать некоторое время, но я вижу две важные возможности:

  1. Если возможно, удалите шаг Base64. Это делает файл больше, а большие данные требуют больше времени для чтения / записи. Существует также стоимость самого преобразования base64.
  2. Не используйте line IO. На самом деле не используйте строки вообще. Поиск разрывов строк и преобразование данных между простыми байтами и string объектами стоит времени, и здесь это бесполезно: работа отменена, и тот факт, что данные были в виде строки ,на самом деле никогда не используется, это просто произвольный способ сортировки данных.

Для более быстрой потоковой копии вы можете использовать, например, IOUtils.copy (in, out) (который также есть в Apache Commons, который, по-видимому, вы уже используете), или же самостоятельно внедрите аналогичную стратегию: считывание блока данных в byte[] (несколько КБ, а не что-то крошечное) и затем запись егов выходной поток, пока все входные данные не будут прочитаны.

...