Оптимизация процесса сжатия 20M файлов с 30 секунд до 1 секунды

Java

Оптимизация процесса сжатия 20M файлов с 30 секунд до 1 секунды

Существует требование, чтобы 10 фотографий, отправленных с внешнего интерфейса, были обработаны серверной частью, а затем сжаты в сжатый пакет и переданы через сетевой поток. С сжатием файлов в Java раньше никогда не сталкивался, поэтому напрямую нашел пример в интернете и изменил его, и его можно использовать после модификации.Также резко возрастает, и в итоге на сжатие ушло 30 секунд 20M файл. Код для сжатого файла выглядит следующим образом.

 1public static void zipFileNoBuffer() {
2    File zipFile = new File(ZIP_FILE);
3    try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) {
4        //开始时间
5        long beginTime = System.currentTimeMillis();
6
7        for (int i = 0; i < 10; i++) {
8            try (InputStream input = new FileInputStream(JPG_FILE)) {
9                zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
10                int temp = 0;
11                while ((temp = input.read()) != -1) {
12                    zipOut.write(temp);
13                }
14            }
15        }
16        printInfo(beginTime);
17    } catch (Exception e) {
18        e.printStackTrace();
19    }
20}

Здесь я нашел изображение 2M и протестировал его десять раз в цикле. Напечатанный результат выглядит следующим образом, время составляет около 30 секунд.

1fileSize:20M
2consum time:29599

Первый процесс оптимизации - с 30 секунд до 2 секунд

Первое, что приходит на ум для оптимизации, это использоватьбуферBufferInputStream. существуетFileInputStreamсерединаread()Метод считывает только один байт за раз. В исходном коде также есть инструкции.

1/**
2 * Reads a byte of data from this input stream. This method blocks
3 * if no input is yet available.
4 *
5 * @return     the next byte of data, or <code>-1</code> if the end of the
6 *             file is reached.
7 * @exception  IOException  if an I/O error occurs.
8 */
9public native int read() throws IOException;

Это вызов собственного метода для взаимодействия с собственной операционной системой для чтения данных с диска. Вызов собственного метода для взаимодействия с операционной системой каждый раз при чтении байта данных занимает очень много времени. Например, теперь у нас есть 30000 байт данных, если мы используемFileInputStreamЗатем вам нужно вызвать собственный метод 30 000 раз, чтобы получить данные, и если вы используете буфер (при условии, что начальный размер буфера достаточен для хранения 30 000 байт данных), вам нужно вызвать его только один раз. потому что буфер вызывается в первомread()Метод будет напрямую считывать данные с диска прямо в память. Затем медленно возвращайте байт за байтом.

BufferedInputStreamБайтовый массив инкапсулирован внутри для хранения данных, размер по умолчанию 8192

Оптимизированный код выглядит следующим образом

 1public static void zipFileBuffer() {
2    File zipFile = new File(ZIP_FILE);
3    try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
4            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(zipOut)) {
5        //开始时间
6        long beginTime = System.currentTimeMillis();
7        for (int i = 0; i < 10; i++) {
8            try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(JPG_FILE))) {
9                zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
10                int temp = 0;
11                while ((temp = bufferedInputStream.read()) != -1) {
12                    bufferedOutputStream.write(temp);
13                }
14            }
15        }
16        printInfo(beginTime);
17    } catch (Exception e) {
18        e.printStackTrace();
19    }
20}

вывод

1------Buffer
2fileSize:20M
3consum time:1808

Видно, что по сравнению с первым использованиемFileInputStreamЭффективность значительно улучшилась

Второй проход оптимизации - с 2 секунд до 1 секунды

использовать буферbufferСлова уже удовлетворили мои потребности, но с идеей применить то, что я узнал, я подумал об использовании знаний в NIO для их оптимизации.

Использовать канал

зачем использоватьChannelШерстяная ткань? Потому что новое в NIOChannelиByteBuffer. Именно потому, что их структура больше соответствует тому, как операционная система выполняет ввод-вывод, их скорость значительно выше по сравнению с традиционным вводом-выводом.Channelкак шахта, содержащая уголь, иByteBufferЭто грузовик, который отправляется в шахту. Другими словами, наше взаимодействие с данными связано сByteBufferвзаимодействие.

Может генерировать в NIOFileChannelЕсть три класса. соответственноFileInputStream,FileOutputStream, и читать и писатьRandomAccessFile.

Исходный код выглядит следующим образом

 1public static void zipFileChannel() {
2    //开始时间
3    long beginTime = System.currentTimeMillis();
4    File zipFile = new File(ZIP_FILE);
5    try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
6            WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) {
7        for (int i = 0; i < 10; i++) {
8            try (FileChannel fileChannel = new FileInputStream(JPG_FILE).getChannel()) {
9                zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));
10                fileChannel.transferTo(0, FILE_SIZE, writableByteChannel);
11            }
12        }
13        printInfo(beginTime);
14    } catch (Exception e) {
15        e.printStackTrace();
16    }
17}

Мы видим, что нет никакой пользы отByteBufferдля передачи данных, но используяtransferToМетоды. Этот метод заключается в прямом соединении двух каналов.

1This method is potentially much more efficient than a simple loop
2* that reads from this channel and writes to the target channel.  Many
3* operating systems can transfer bytes directly from the filesystem cache
4* to the target channel without actually copying them. 

Это текст описания исходного кода, который, вероятно, означает использованиеtransferToболее эффективен, чем зацикливаниеChannelпрочитайте его, а затем зациклите, чтобы написать другойChannelхорошо. Операционная система может передавать байты непосредственно из кеша файловой системы в целевойChannel, не требуя фактическогоcopyсцена.

Этап копирования — это процесс перехода из пространства ядра в пространство пользователя.

Видно, что скорость несколько улучшилась по сравнению с использованием буферов.

1------Channel
2fileSize:20M
3consum time:1416

Пространство ядра и пространство пользователя

Так почему же процесс из пространства ядра в пространство пользователя идет медленно? Первое, что нам нужно понять, это что такое пространство ядра и пространство пользователя. В обычной операционной системе, чтобы защитить основные ресурсы в системе, система разделена на четыре области, и права доступа увеличиваются по мере того, как вы входите, поэтому Ring0 называется пространством ядра, которое используется для доступа к некоторым ключам. Ресурсы. Ring3 называется пользовательским пространством.

Пользовательский режим, режим ядра: поток в пространстве ядра называется режимом ядра, а поток в пространстве пользователя принадлежит пользовательскому режиму.

Так что, если нам нужен доступ к основным ресурсам в то время, когда приложению (все приложения относятся к пользовательскому режиму) нужен доступ к основным ресурсам? Затем вам нужно вызвать интерфейс, представленный в ядре, для вызова, который называетсясистемный вызов. Например, в это время нашему приложению требуется доступ к файлам на диске. В этот момент приложение вызовет интерфейс системного вызоваopenЗатем ядро ​​обращается к файлу на диске и возвращает содержимое файла приложению. Общий процесс выглядит следующим образом

Прямые и косвенные буферы

Поскольку мы хотим прочитать файл с диска, нам приходится тратить такую ​​большую неудачу. Есть ли простой способ позволить нашему приложению напрямую манипулировать файлами на диске без необходимости передачи ядра? Да, то есть создать прямой буфер.

  • Непрямой буфер: непрямой буфер — это режим ядра, о котором мы упоминали выше в качестве посредника, и ядро ​​каждый раз должно быть посередине в качестве ретранслятора.

  • Прямой буфер: Прямой буфер не требует пространства ядра в качестве данных транзитной копии, но напрямую применяется для пространства в физической памяти.Это пространство отображается в адресное пространство ядра и адресное пространство пользователя, и доступ к данным между приложениями и дисками осуществляется через это напрямую. прикладная физическая память взаимодействует.

Поскольку прямые буферы такие быстрые, почему мы все не используем прямые буферы? Фактически прямой буфер имеет следующие недостатки. Недостатки прямых буферов:

  1. небезопасный
  2. Потребляет больше, потому что напрямую не открывает пространство в JVM. Утилизация этой части памяти может зависеть только от механизма сборки мусора, и когда мусор утилизируется, мы не контролируем.
  3. Когда данные записываются в буфер физической памяти, программа теряет управление этими данными, то есть когда данные окончательно записываются на диск, может определить только операционная система, и прикладная программа уже не может вмешиваться.

Подводя итог, мы используемtransferToМетод заключается в непосредственном открытии прямого буфера. Таким образом, производительность значительно улучшилась

Использовать файлы с отображением памяти

Еще одна новая функция в NIO — файлы с отображением памяти.Почему файлы с отображением памяти работают быстро? На самом деле причина та же, что и выше, а именно открытие прямого буфера в памяти. Взаимодействуйте напрямую с данными. Исходный код выглядит следующим образом

 1//Version 4 使用Map映射文件
2public static void zipFileMap() {
3    //开始时间
4    long beginTime = System.currentTimeMillis();
5    File zipFile = new File(ZIP_FILE);
6    try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
7            WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) {
8        for (int i = 0; i < 10; i++) {
9
10            zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));
11
12            //内存中的映射文件
13            MappedByteBuffer mappedByteBuffer = new RandomAccessFile(JPG_FILE_PATH, "r").getChannel()
14                    .map(FileChannel.MapMode.READ_ONLY, 0, FILE_SIZE);
15
16            writableByteChannel.write(mappedByteBuffer);
17        }
18        printInfo(beginTime);
19    } catch (Exception e) {
20        e.printStackTrace();
21    }
22}

печатать следующим образом

1---------Map
2fileSize:20M
3consum time:1305

Вы можете видеть, что скорость аналогична скорости использования Channel.

Использовать трубу

Канал Java NIO — это одностороннее соединение данных между двумя потоками. Труба имеет канал источника и канал стока. Канал источника используется для чтения данных, а канал приемника — для записи данных. Вы можете увидеть введение в исходном коде, что, вероятно, означает, что поток записи будет заблокирован до тех пор, пока поток чтения не прочитает данные из канала. Если данных для чтения нет, поток чтения также будет заблокирован до тех пор, пока поток записи не запишет данные. пока канал не закроется.

1 Whether or not a thread writing bytes to a pipe will block until another
2 thread reads those bytes

Эффект, который я хочу, заключается в следующем. Исходный код выглядит следующим образом

 1//Version 5 使用Pip
2public static void zipFilePip() {
3
4    long beginTime = System.currentTimeMillis();
5    try(WritableByteChannel out = Channels.newChannel(new FileOutputStream(ZIP_FILE))) {
6        Pipe pipe = Pipe.open();
7        //异步任务
8        CompletableFuture.runAsync(()->runTask(pipe));
9
10        //获取读通道
11        ReadableByteChannel readableByteChannel = pipe.source();
12        ByteBuffer buffer = ByteBuffer.allocate(((int) FILE_SIZE)*10);
13        while (readableByteChannel.read(buffer)>= 0) {
14            buffer.flip();
15            out.write(buffer);
16            buffer.clear();
17        }
18    }catch (Exception e){
19        e.printStackTrace();
20    }
21    printInfo(beginTime);
22
23}
24
25//异步任务
26public static void runTask(Pipe pipe) {
27
28    try(ZipOutputStream zos = new ZipOutputStream(Channels.newOutputStream(pipe.sink()));
29            WritableByteChannel out = Channels.newChannel(zos)) {
30        System.out.println("Begin");
31        for (int i = 0; i < 10; i++) {
32            zos.putNextEntry(new ZipEntry(i+SUFFIX_FILE));
33
34            FileChannel jpgChannel = new FileInputStream(new File(JPG_FILE_PATH)).getChannel();
35
36            jpgChannel.transferTo(0, FILE_SIZE, out);
37
38            jpgChannel.close();
39        }
40    }catch (Exception e){
41        e.printStackTrace();
42    }
43}

Суммировать

  • Жизни нужно учиться везде, и иногда это просто простая оптимизация, позволяющая глубоко изучить множество различных знаний. Поэтому в исследовании мы не должны просить большего понимания, не только знать это знание, но и понимать, зачем мы это делаем.
  • Единство знания и действия: изучив знание, попытайтесь применить его снова. Вот как вы можете надежно запомнить его.

Адрес источника

Справочная статья