Java FileChannel против BufferedReader — Spring Batch — Reader

Мы обрабатываем огромные файлы (иногда по 50 ГБ каждый файл). Приложение читает этот файл и на основе бизнес-логики записывает несколько выходных файлов (4-6).

Записи в файле имеют переменную длину, и каждое поле в записи отделено разделителем.

Исходя из понимания того, что чтение файла с использованием FileChannel с ByteBuffer всегда было лучше, чем использование BufferedReader.readLine с последующим использованием разделения по разделителю.

  • BufferSizes пробовал 10240(10КБ) и даже больше
  • Интервал фиксации - 5000, 10000 и т.д.

Ниже показано, как мы использовали файловый канал для чтения:

  • Читать байт за байтом. Проверьте, является ли прочитанный байт новой строкой char(10), что означает конец строки.
  • проверьте байты-разделители. захватить байты, прочитанные в массиве байтов (мы инициализировали этот массив байтов с максимальным размером поля 350 байтов), пока не будут обнаружены байты-разделители.
  • преобразовать эти байты, прочитанные до этого времени, в строку с использованием кодировки UTF-8 - новая строка (byteArr, 0, индекс, "UTF-8"), чтобы быть конкретным - индекс - это количество байтов, прочитанных до разделителя.

При использовании этого метода чтения файла с помощью FileChannel обработка файла заняла 57 минут.

Мы хотим уменьшить это время и попытались использовать BufferredReader.readLine(), а затем использовать разделение по разделителю, чтобы увидеть, как это работает.

И поразительно тот же файл завершил обработку всего за 7 минут.

В чем тут подвох? Почему FileChannel занимает больше времени, чем буферизованный ридер, а затем использует разделение строк.

Я всегда предполагал, что комбинация ReadLine и Split сильно повлияет на производительность?

Может ли кто-нибудь пролить свет, если я неправильно использовал FileChannel? Один

Заранее спасибо. Надеюсь, я правильно изложил суть вопроса.

Ниже приведен пример кода:

while (inputByteBuffer.hasRemaining() && (b = inputByteBuffer.get()) != 0){
        boolean endOfField = false;
        if (b == 10){
            break;
        }
        else{
            if (b == 94){//^
                if (!inputByteBuffer.hasRemaining()){
                    inputByteBuffer.clear();
                    noOfBytes = inputFileChannel.read(inputByteBuffer);
                    inputByteBuffer.flip();
                }
                if (inputByteBuffer.hasRemaining()){
                    byte b2 = inputByteBuffer.get();
                    if (b2 == 124){//|
                        if (!inputByteBuffer.hasRemaining()){
                            inputByteBuffer.clear();
                            noOfBytes = inputFileChannel.read(inputByteBuffer);
                            inputByteBuffer.flip();
                        }

                        if (inputByteBuffer.hasRemaining()){
                            byte b3 = inputByteBuffer.get();
                            if (b3 == 94){//^
                                String field = new String(fieldBytes, 0, index, encoding);
                                if(fieldIndex == -1){
                                    fields = new String[sizeFromAConfiguration];
                                }else{
                                    fields[fieldIndex] = field;
                                }

                                fieldBytes = new byte[maxFieldSize];
                                endOfField = true;
                                fieldIndex++;
                            }
                            else{
                                fieldBytes = addFieldBytes(fieldBytes, b, index);
                                index++;
                                fieldBytes = addFieldBytes(fieldBytes, b2, index);
                                index++;
                                fieldBytes = addFieldBytes(fieldBytes, b3, index);
                            }
                        }
                        else{
                            endOfFile = true;
                            //fields.add(new String(fieldBytes, 0, index, encoding));
                            fields[fieldIndex] = new String(fieldBytes, 0, index, encoding);
                            fieldBytes = new byte[maxFieldSize];
                            endOfField = true;
                        }
                    }else{
                        fieldBytes = addFieldBytes(fieldBytes, b, index);
                        index++;
                        fieldBytes = addFieldBytes(fieldBytes, b2, index);

                    }
                }else{
                    endOfFile = true;
                    fieldBytes = addFieldBytes(fieldBytes, b, index);
                }
            }
            else{
                fieldBytes = addFieldBytes(fieldBytes, b, index);
            }
        }

        if (!inputByteBuffer.hasRemaining()){
            inputByteBuffer.clear();
            noOfBytes = inputFileChannel.read(inputByteBuffer);
            inputByteBuffer.flip();
        }

        if (endOfField){
            index = 0;
        }
        else{
            index++;
        }

    }

person Sanjeev    schedule 02.03.2018    source источник
comment
BufferedReader не читает побайтно, и вы не должны. Вы должны выбрать буфер разумного размера (BufferedReader имеет буфер размером 8192 байта). Да, это будет сложнее реализовать, но вы не будете тратить циклы процессора на чтение одного байта за раз.   -  person Jake Holzinger    schedule 02.03.2018
comment
Чтение любого файла побайтно — худший случай. Что-нибудь будет улучшением по сравнению с этим. readLine() примерно в лучшем случае. Следует избегать разделения или, скорее, создания строк.   -  person user207421    schedule 02.03.2018
comment
Возможно, я неправильно выразился, когда я сказал читать побайтно, мы сначала читаем 10240 байт в байтовый буфер, а затем метод ByteBuffer.get() проверяет, какой это байт.   -  person Sanjeev    schedule 02.03.2018
comment
Поскольку вы явно не можете точно описать это, вам обязательно следует опубликовать код.   -  person user207421    schedule 02.03.2018
comment
если (!inputByteBuffer.hasRemaining()){ inputByteBuffer.clear(); noOfBytes = inputFileChannel.read (inputByteBuffer); inputByteBuffer.flip(); }   -  person Sanjeev    schedule 02.03.2018
comment
Нет. В вашем вопросе. И этого сегмента недостаточно. Нужно показать обработку.   -  person user207421    schedule 02.03.2018
comment
Если вы правильно буферизуете, то проблема, безусловно, заключается в постобработке. Я не уверен, почему постобработка такая тяжеловесная, но если ее нельзя улучшить, вы можете разделять и властвовать. После того, как вы подготовили несколько байтов для обработки, поставьте их в очередь для обработки в пуле потоков, а не в основном потоке.   -  person Jake Holzinger    schedule 02.03.2018
comment
Я фактически измерил время, необходимое для чтения (чтения), обработки и записи. 90% времени уходит на чтение файла.   -  person Sanjeev    schedule 02.03.2018
comment
Должен ли я делать bytebuffer.allocate каждый раз, когда очищаю буфер? Я надеюсь, что вызов остаточного буфера перед каждым получением не окажет негативного влияния?   -  person Sanjeev    schedule 02.03.2018
comment
Пробовал читать байт за байтом с помощью bufferedreader, даже это занимает больше времени.   -  person Sanjeev    schedule 02.03.2018


Ответы (4)


Вы вызываете много накладных расходов с постоянными проверками hasRemaining()/read(), а также с постоянными вызовами get(). Вероятно, было бы лучше get() весь буфер превратить в массив и обработать его напрямую, вызывая read() только тогда, когда вы дойдете до конца.

И чтобы ответить на вопрос в комментариях, вы не должны выделять новый ByteBuffer за чтение. Это дорого. Продолжайте использовать тот же. И обратите внимание, не используйте DirectByteBuffer для этого приложения. Это не подходит: это подходит только тогда, когда вы хотите, чтобы данные оставались к югу от границы JVM/JNI, например. при простом копировании между каналами.

Но я думаю, что я бы выбросил это или, скорее, переписал бы его, используя BufferedReader.read(), а не readLine(), за которым следует разбиение строки, и используя почти ту же логику, что и здесь, за исключением того, что вам не нужно продолжать вызывать hasRemaining() и заполнение буфера, что BufferedReader сделает за вас автоматически.

Вы должны позаботиться о том, чтобы сохранить результат read() в int и проверять его на -1 после каждого read().

Мне не ясно, что вы вообще должны использовать Reader, если только вы не знаете, что у вас многобайтовый текст. Возможно, простое BufferedInputStream было бы более подходящим.

person user207421    schedule 02.03.2018
comment
Спасибо за ваше подробное объяснение... наш файл закодирован в UTF-8, и есть специальные символы, которые символ может составлять до двух байтов... и мы должны преобразовать в строку, чтобы выполнить некоторую бизнес-логику. - person Sanjeev; 02.03.2018
comment
Я бы попробовал приведенные выше предложения и разместил здесь информацию о любых улучшениях/выводах. - person Sanjeev; 02.03.2018
comment
Я попытался скопировать содержимое байтового буфера в byte[], а затем выполнить итерацию по byte[], но похоже, что это занимает еще больше времени, чем раньше: if (byteArrayIndex == 0 || byteArrayIndex == byteArray.length){ inputByteBuffer.clear( ); байтовый индекс массива = 0; int noOfBytes = inputFileChannel.read (inputByteBuffer); inputByteBuffer.flip(); массив байтов = новый байт[noOfBytes]; inputByteBuffer.get (byteArray, 0, noOfBytes); } - person Sanjeev; 02.03.2018
comment
Одной из причин может быть то, что первоначальный автор кода не профилировал код. :) - person Harish; 03.03.2018

Хотя нельзя с уверенностью сказать, как будет вести себя конкретный код, я полагаю, что лучший способ - профилировать его так же, как и вы. FileChannel, хотя и считается более быстрым, на самом деле не помогает в вашем случае. Но это может быть не из-за чтения из файла, а фактическая обработка, которую вы выполняете с контентом, который вы читаете. При работе с файлами я хотел бы отметить одну статью: https://www.redgreencode.com/why-is-java-io-slow/

Также соответствующая кодовая база Github тест Java IO

Я хотел бы указать, что этот код использует комбинацию обоих миров fos = new FileOutputStream(outputFile); outFileChannel = fos.getChannel(); bufferedWriter = new BufferedWriter(Channels.newWriter(outFileChannel, "UTF-8"));

Поскольку это читается в вашем случае, я рассмотрю

File inputFile = new File("C:\\input.txt");
FileInputStream fis = new FileInputStream(inputFile);
FileChannel inputChannel = fis.getChannel();
BufferedReader bufferedReader = new BufferedReader(Channels.newReader(inputChannel,"UTF-8"));

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

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

import java.io.UnsupportedEncodingException;

public class EbcdicConvertor {

    public static void main(String[] args) throws UnsupportedEncodingException {
        int index = 0;
        for (int i = -127; i < 128; i++) {
            byte[] b = new byte[1];
            b[0] = (byte) i;
            String cp037 = new String(b, "CP037");
            if (cp037.getBytes().length == 2) {
                index++;
                System.out.println(i + "::" + cp037);
            }
        }
        System.out.println(index);
    }
}

Приведенный выше ответ не проверяет мою фактическую гипотезу. Вот реальная программа для измерения времени. Результаты говорят сами за себя в файле размером 200 МБ.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;

public class ReadComplexDelimitedFile {
    private static long total = 0;
    private static final Pattern DELIMITER_PATTERN = Pattern.compile("\\^\\|\\^");

    private void readFileUsingScanner() {

        String s;
        try (Scanner stdin = new Scanner(new File(this.getClass().getResource("input.txt").getPath()))) {
            while (stdin.hasNextLine()) {
                s = stdin.nextLine();
                String[] fields = DELIMITER_PATTERN.split(s, 0);
                total = total + fields.length;
            }
        } catch (Exception e) {
            System.err.println("Error");
        }

    }

    private void readFileUsingCustomBufferedReader() {

        try (BufferedReader stdin = new BufferedReader(new FileReader(new File(this.getClass().getResource("input.txt").getPath())))) {
            String s;
            while ((s = stdin.readLine()) != null) {
                String[] fields = DELIMITER_PATTERN.split(s, 0);
                total += fields.length;
            }
        } catch (Exception e) {
            System.err.println("Error");
        }

    }

    private void readFileUsingBufferedReader() {

        try (java.io.BufferedReader stdin = new java.io.BufferedReader(new FileReader(new File(this.getClass().getResource("input.txt").getPath())))) {
            String s;
            while ((s = stdin.readLine()) != null) {
                String[] fields = DELIMITER_PATTERN.split(s, 0);
                total += fields.length;
            }
        } catch (Exception e) {
            System.err.println("Error");
        }

    }


    private void readFileUsingBufferedReaderFileChannel() {
        try (FileInputStream fis = new FileInputStream(this.getClass().getResource("input.txt").getPath())) {
            try (FileChannel inputChannel = fis.getChannel()) {
                try (BufferedReader stdin = new BufferedReader(Channels.newReader(inputChannel, "UTF-8"))) {
                    String s;
                    while ((s = stdin.readLine()) != null) {
                        String[] fields = DELIMITER_PATTERN.split(s, 0);
                        total = total + fields.length;
                    }
                }
            } catch (Exception e) {
                System.err.println("Error");
            }
        } catch (Exception e) {
            System.err.println("Error");
        }

    }

    private void readFileUsingBufferedReaderByteFileChannel() {
        try (FileInputStream fis = new FileInputStream(this.getClass().getResource("input.txt").getPath())) {
            try (FileChannel inputChannel = fis.getChannel()) {
                try (BufferedReader stdin = new BufferedReader(Channels.newReader(inputChannel, "UTF-8"))) {
                    int b;
                    StringBuilder sb = new StringBuilder();
                    while ((b = stdin.read()) != -1) {
                        if (b == 10) {

                            total = total + DELIMITER_PATTERN.split(sb, 0).length;
                            sb = new StringBuilder();
                        } else {
                            sb.append((char) b);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            System.err.println("Error");
        }

    }

    private void readFileUsingFileChannelStream() {

        try (RandomAccessFile fis = new RandomAccessFile(new File(this.getClass().getResource("input.txt").getPath()), "r")) {
            try (FileChannel inputChannel = fis.getChannel()) {
                ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
                ByteBuffer recordBuffer = ByteBuffer.allocate(250);
                int recordLength = 0;
                while ((inputChannel.read(byteBuffer)) != -1) {
                    byte b;
                    byteBuffer.flip();
                    while (byteBuffer.hasRemaining() && (b = byteBuffer.get()) != -1) {
                        if (b == 10) {
                            recordBuffer.flip();
                            total = total + splitIntoFields(recordBuffer, recordLength);
                            recordBuffer.clear();
                            recordLength = 0;
                        } else {
                            ++recordLength;
                            recordBuffer.put(b);
                        }
                    }
                    byteBuffer.clear();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private int splitIntoFields(ByteBuffer recordBuffer, int recordLength) {
        byte b;
        String[] fields = new String[17];
        int fieldCount = -1;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < recordLength - 1; i++) {
            b = recordBuffer.get(i);
            if (b == 94 && recordBuffer.get(++i) == 124 && recordBuffer.get(++i) == 94) {
                fields[++fieldCount] = sb.toString();
                sb = new StringBuilder();
            } else {
                sb.append((char) b);
            }
        }
        fields[++fieldCount] = sb.toString();
        return fields.length;

    }


    public static void main(String args[]) {
        //JVM wamrup
        for (int i = 0; i < 100000; i++) {
            total += i;
        }
        // We know scanner is slow-Still warming up
        ReadComplexDelimitedFile readComplexDelimitedFile = new ReadComplexDelimitedFile();
        List<Long> longList = new ArrayList<>(50);
        for (int i = 0; i < 50; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingScanner();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingScanner");
        longList.forEach(System.out::println);
        // Actual performance test starts here

        longList = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingBufferedReaderFileChannel();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingBufferedReaderFileChannel");
        longList.forEach(System.out::println);
        longList.clear();
        for (int i = 0; i < 10; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingBufferedReader();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingBufferedReader");
        longList.forEach(System.out::println);
        longList.clear();
        for (int i = 0; i < 10; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingCustomBufferedReader();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingCustomBufferedReader");
        longList.forEach(System.out::println);
        longList.clear();
        for (int i = 0; i < 10; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingBufferedReaderByteFileChannel();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingBufferedReaderByteFileChannel");
        longList.forEach(System.out::println);
        longList.clear();
        for (int i = 0; i < 10; i++) {
            total = 0;
            long startTime = System.nanoTime();
            readComplexDelimitedFile.readFileUsingFileChannelStream();
            long stopTime = System.nanoTime();
            long timeDifference = stopTime - startTime;
            longList.add(timeDifference);

        }
        System.out.println("Time taken for readFileUsingFileChannelStream");
        longList.forEach(System.out::println);

    }
}

BufferedReader был написан очень давно, и поэтому мы можем переписать некоторые части, относящиеся к этому примеру. Например, нам не нужны \r и skipLF или skipCR или подобные вещи. Мы собираемся прочитать файл (нет необходимости в синхронизации) По расширению нет необходимости в StringBuffer, даже если в противном случае можно использовать StringBuilder. Сразу видно улучшение производительности.

опасный взлом, удалите синхронизированный и замените StringBuffer на StringBuilder, не используйте его без надлежащего тестирования и не зная, что вы делаете

public String readLine() throws IOException {
        StringBuilder s = null;
        int startChar;


        bufferLoop:
        for (; ; ) {

            if (nextChar >= nChars)
                fill();
            if (nextChar >= nChars) { /* EOF */
                if (s != null && s.length() > 0)
                    return s.toString();
                else
                    return null;
            }
            boolean eol = false;
            char c = 0;
            int i;

            /* Skip a leftover '\n', if necessary */


            charLoop:
            for (i = nextChar; i < nChars; i++) {
                c = cb[i];
                if (c == '\n') {
                    eol = true;
                    break charLoop;
                }
            }

            startChar = nextChar;
            nextChar = i;

            if (eol) {
                String str;
                if (s == null) {
                    str = new String(cb, startChar, i - startChar);
                } else {
                    s.append(cb, startChar, i - startChar);
                    str = s.toString();
                }
                nextChar++;
                return str;
            }

            if (s == null)
                s = new StringBuilder(defaultExpectedLineLength);
            s.append(cb, startChar, i - startChar);
        }
    }

Java 8 Intel i5 12 ГБ ОЗУ Windows 10 Результат:

Время, затраченное на readFileUsingBufferedReaderFileChannel::

  • 2581635057 1849820885 1763992972 1770510738 1746444157 1733491399 1740530125 1723907177 1724280512 1732445638

Время, затраченное на readFileUsingBufferedReader

  • 1851027073 1775304769 1803507033 1789979554 1786974538 1802675458 1789672780 1798036307 1789847714 1785302003

Время, затраченное на readFileUsingCustomBufferedReader

  1. 1745220476 1721039975 1715383650 1728548462 1724746005 1718177466 1738026017 1748077438 1724608192 1736294175

Время, затраченное на readFileUsingBufferedReaderByteFileChannel

  • 2872857919 2480237636 2917488143 2913491126 2880117231 2904614745 2911756298 2878777496 2892169722 2888091211

Время, затраченное на readFileUsingFileChannelStream

  • 3039447073 2896156498 2538389366 2906287280 2887612064 2929288046 2895626578 2955326255 2897535059 2884476915

Процесс завершен с кодом выхода 0

person Harish    schedule 03.03.2018
comment
FileChannel не имеет «неблокирующего характера». - person user207421; 06.03.2018
comment
Мой плохой, отредактировано - person Harish; 07.03.2018
comment
В любом случае, очевидно, что строка чтения bufferedReader работает быстрее, чем все другие методы, основанные на моем профилировании. - person Harish; 07.03.2018

Я попробовал NIO со всеми возможными вариантами (предоставленными в этом посте и насколько я знаю и исследовал) и обнаружил, что он нигде не приближается к BufferedReader с точки зрения чтения текстового файла.

Изменив BufferedReader на использование StringBuilder вместо StringBuffer, я не вижу значительного улучшения производительности (всего несколько секунд для некоторых файлов, а некоторые из них лучше использовать сам StringBuffer).

Удаление синхронизированного блока также не дало особых улучшений. И не стоит настраивать то, чем мы не получили никакой пользы.

Ниже показано время (чтение, обработка, запись — время, затраченное на обработку и запись, незначительно — даже не 20% времени) для файла размером около 50 ГБ. NIO: 71,67 (минуты) IO (BufferedReader): 10,84 ( минут)

Спасибо всем за ваше время, чтобы прочитать и ответить на этот пост и дать предложения.

person Sanjeev    schedule 16.03.2018

Основная проблема здесь заключается в очень быстром создании нового byte[] (fieldBytes = new byte[maxFieldSize];).

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

Кроме того, создание объекта может быть дорогостоящим.

Мы могли бы сначала инициализировать массив байтов, а затем отслеживать индексы, чтобы просто преобразовать поле в строку с конечным индексом.

В любом случае, BufferedReader работает быстрее, чем FileChannel, по крайней мере, для чтения файлов ASCII, и для простоты кода мы продолжали использовать сам Bufferred Reader.

Используя Bufferred reader, усилия по разработке и тестированию могут быть уменьшены за счет отсутствия утомительной логики для поиска разделителей и заполнения объекта.

person Sanjeev    schedule 01.02.2019