2016-12-25 4 views
38

Мне нужно прочитать файл по одному символу за раз, и я использую метод read() от BufferedReader. *Почему BufferedReader read() намного медленнее, чем readLine()?

Я обнаружил, что read() примерно в 10 раз медленнее, чем readLine(). Ожидается ли это? Или я делаю что-то неправильно?

Вот тест с Java 7. Тест входной файл имеет около 5 миллионов строк и 254 миллионов символов (~ 242 Мб) **:

Метод read() занимает около 7000 мс, чтобы прочитать все символы:

@Test 
public void testRead() throws IOException, UnindexableFastaFileException{ 

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); 

    long t0= System.currentTimeMillis(); 
    int c; 
    while((c = fa.read()) != -1){ 
     // 
    } 
    long t1= System.currentTimeMillis(); 
    System.err.println(t1-t0); // ~ 7000 ms 

} 

метод readLine() занимает всего ~ 700 мс:

@Test 
public void testReadLine() throws IOException{ 

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa"))); 

    String line; 
    long t0= System.currentTimeMillis(); 
    while((line = fa.readLine()) != null){ 
     // 
    } 
    long t1= System.currentTimeMillis(); 
    System.err.println(t1-t0); // ~ 700 ms 
} 

* Практическое назначение: Мне нужно знать длину каждой строки, включая символы новой строки (\n или \r\n) И длину линии после ее снятия. Мне также нужно знать, начинается ли строка с символа >. Для данного файла это делается только один раз в начале программы. Поскольку символы EOL не возвращаются BufferedReader.readLine() Я прибегаю к методу read(). Если есть лучшие способы сделать это, скажите.

** Файл с gzip находится здесь http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz. Для тех, кому может быть интересно, я пишу класс для индексации файлов fasta.

+11

Прочитайте, как писать точные тесты Java. –

+6

@Louis Wasserman По общему признанию, я не слишком заботился о том, чтобы быть точным в моих тестах. JUnit и 'currentTimeMillis()' не идеальны, но я решил, что разница в 8-10 раз в довольно большом файле достаточно велика, чтобы задать вопрос. – dariober

+1

@ dariober Возможно, вам лучше использовать 'public int read (char [] cbuf, int off, int len) throws IOException' вместо прямого использования' read' функции bufferdreader. В конечном счете ваша цель - найти конец строк в файле. Хотя я сам его не тестировал, но контроль над буфером в руке, вероятно, даст вам лучший результат. –

ответ

34

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

С одной стороны, мы должны учитывать, что, поскольку современные операционные системы любят кэшировать данные файла, к которым регулярно обращаются, нам нужен способ очистки кэшей между тестами. В Windows есть небольшая небольшая утилита that does just this - на Linux вы сможете это сделать, написав где-нибудь какой-нибудь псевдофайл.

Затем код выглядит следующим образом:

import org.openjdk.jmh.annotations.Benchmark; 
import org.openjdk.jmh.annotations.BenchmarkMode; 
import org.openjdk.jmh.annotations.Fork; 
import org.openjdk.jmh.annotations.Mode; 

import java.io.BufferedReader; 
import java.io.FileReader; 
import java.io.IOException; 

@BenchmarkMode(Mode.AverageTime) 
@Fork(1) 
public class IoPerformanceBenchmark { 
    private static final String FILE_PATH = "test.fa"; 

    @Benchmark 
    public int readTest() throws IOException, InterruptedException { 
     clearFileCaches(); 
     int result = 0; 
     try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { 
      int value; 
      while ((value = reader.read()) != -1) { 
       result += value; 
      } 
     } 
     return result; 
    } 

    @Benchmark 
    public int readLineTest() throws IOException, InterruptedException { 
     clearFileCaches(); 
     int result = 0; 
     try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { 
      String line; 
      while ((line = reader.readLine()) != null) { 
       result += line.chars().sum(); 
      } 
     } 
     return result; 
    } 

    private void clearFileCaches() throws IOException, InterruptedException { 
     ProcessBuilder pb = new ProcessBuilder("EmptyStandbyList.exe", "standbylist"); 
     pb.inheritIO(); 
     pb.start().waitFor(); 
    } 
} 

и если мы запустим его с

chcp 65001 # set codepage to utf-8 
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar 

мы получаем следующие результаты (около 2 секунд, необходимо очистить кэш-память для меня, и я бегу это на жесткий диск так, поэтому это хорошая сделка медленнее, чем для вас):

Benchmark       Mode Cnt Score Error Units 
IoPerformanceBenchmark.readLineTest avgt 20 3.749 ± 0.039 s/op 
IoPerformanceBenchmark.readTest  avgt 20 3.745 ± 0.023 s/op 

Сюрприз! Как и ожидалось, здесь нет никакой разницы в производительности после того, как JVM заработала стабильный режим. Но есть один останец в методе readCharTest:

# Warmup Iteration 1: 6.186 s/op 
# Warmup Iteration 2: 3.744 s/op 

который exaclty проблема, что вы видите. Наиболее вероятная причина, по которой я могу думать, заключается в том, что OSR не делает здесь хорошую работу или что JIT работает слишком поздно, чтобы изменить ситуацию на первой итерации.

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

Решение этой проблемы непросто и нет общих решений, хотя есть способы справиться с этим.Один простой тест, чтобы убедиться, что мы на правильном пути, - это запустить код с параметром -Xcomp, который заставляет HotSpot компилировать каждый метод при первом вызове. И в самом деле делать это, вызывает большие задержки при первом вызове исчезнуть:

# Warmup Iteration 1: 3.965 s/op 
# Warmup Iteration 2: 3.753 s/op 

Возможное решение

Теперь, когда мы имеем хорошее представление о том, что фактическая проблема (я думаю, до сих пор все те, блокировки не объединены и не используют эффективную реализацию смещенных блокировок), решение довольно прямолинейно и просто: уменьшите количество вызовов функций (так что да, мы могли бы прийти к этому решению без всего выше, но всегда приятно иметь хорошо справиться с проблемой, и, возможно, было решение, которое не связано с изменением кода).

Следующий код работает последовательно быстрее, чем любой из двух других - вы можете играть с размером массива, но это удивительно неважно (по-видимому, потому что в отличие от других методов read(char[]) не нужно приобретать блокировку, поэтому стоимость за звонок ниже для начала).

private static final int BUFFER_SIZE = 256; 
private char[] arr = new char[BUFFER_SIZE]; 

@Benchmark 
public int readArrayTest() throws IOException, InterruptedException { 
    clearFileCaches(); 
    int result = 0; 
    try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) { 
     int charsRead; 
     while ((charsRead = reader.read(arr)) != -1) { 
      for (int i = 0; i < charsRead; i++) { 
       result += arr[i]; 
      } 
     } 
    } 
    return result; 
} 

Это, скорее всего, достаточно хорошо производительности мудрая, но если вы хотите повысить производительность еще больше, используя file mapping мощи (не будет рассчитывать на слишком большое улучшение в случае, например, как это, но если вы знаете, что ваш текст всегда ASCII, вы можете сделать некоторые дальнейшие оптимизации), а также повысить производительность.

+0

readCharTest должен быть 'readTest()'? (Я скоро удалю этот комментарий) – Marco13

+0

Хорошие новости! Я смог воспроизвести ваши результаты, но я думаю, что введенный шум делает их в значительной степени недействительными - вы измеряете очистку кеша, и вы добавили неэквивалентную обработку, которая переполняет фактически измеренную вещь. У меня есть две общие критические замечания: одна из них (более «основанная на мнениях») заключается в том, что на самом деле это не микробиблиотека, поэтому сама методология не является репрезентативной. Другой, даже если мы принимаем методологию, не так сложно выявить различия в производительности между 50% и 300% - то есть эти конкретные измерения не являются репрезентативными. – pvg

+0

Я постараюсь завтра написать свои результаты и опубликовать их. – pvg

0

Это неудивительно, если вы думаете об этом. Один тест - итерация строк в текстовом файле, а другая - итерация символов.

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

Если вы действительно хотите проверить разницу между 2, я бы предложил установку, в которой вы выполняете итерацию по каждому символу в обоих тестах. Например. что-то вроде:

void readTest(BufferedReader r) 
{ 
    int c; 
    StringBuilder b = new StringBuilder(); 
    while((c = r.read()) != -1) 
     b.append((char)c); 
} 

void readLineTest(BufferedReader r) 
{ 
    String line; 
    StringBuilder b = new StringBuilder(); 
    while((line = b.readLine())!= null) 
     for(int i = 0; i< line.length; i++) 
      b.append(line.charAt(i)); 
} 

Помимо вышеуказанного, используйте «Инструмент диагностики производительности Java», чтобы сравнить ваш код. Также читаем how to microbenchmark java code.

+4

Это не микробиблиотека. Подход плакатов, какой бы примитивной она ни была, не является необоснованной для временных и временных соотношений. Вы можете использовать команду unix time для этого с достаточной уверенностью, что вы видите значительный эффект. – pvg

-1

Java JIT оптимизирует прочь пустые тела цикла, так что ваши петли на самом деле выглядеть следующим образом:

while((c = fa.read()) != -1); 

и

while((line = fa.readLine()) != null); 

Я предлагаю вам прочитать на бенчмаркинг here и оптимизации контуров here ,


Как почему время принято различен:

  • Причина одна (Это относится только тогда, когда органы петлями содержат код): В первом примере, вы делаете один операция на линию, во втором - вы на один символ. Это добавляет больше строк/символов, которые у вас есть.

    while((c = fa.read()) != -1){ 
        //One operation per character. 
    } 
    
    while((line = fa.readLine()) != null){ 
        //One operation per line. 
    } 
    
  • Причина два: В классе BufferedReader, метод readLine() не использует read() за кадром - он использует свой собственный код. Метод readLine() делает меньше операций для каждого символа для чтения строки, чем требуется для чтения строки с помощью метода read() - вот почему readLine() быстрее читает весь файл.

  • Причина три: Требуется больше итераций для чтения каждого символа, чем чтение каждой строки (если только каждый символ не находится на новой строке); read() называется больше раз, чем readLine().

+2

Если java оптимизировал эти петли, не было бы разницы во времени. – pvg

+0

@pvg См. Редактирование. 'read' и' readLine' читают файл по-разному. И их все еще называют в петлях. –

+0

Я не думаю, что пустая петля имеет значение. Я помещаю 'if (line.contains ("> ")) {System.out.println (строка); } 'внутри цикла теста readLine() и' if (c == '>') {System.out.println (c); }; 'внутри read(). Результаты остаются неизменными. – dariober

1

Спасибо @Voo за исправление. То, что я упомянул ниже, является правильным с FileReader#read() v/s BufferedReader#readLine() Точка зрения, если вы не согласны с BufferedReader#read() v/s BufferedReader#readLine() Точка зрения, поэтому я получил ответ.

Использование метода read() на BufferedReader не является хорошей идеей, это не причинит вам никакого вреда, но это, безусловно, означает, что цель класса.

Целевая цель в жизни BufferedReader заключается в уменьшении ввода-вывода путем буферизации содержимого. Вы можете прочитать here в учебниках по Java. Вы также можете заметить, что метод read() в BufferedReader фактически унаследован от Reader, а readLine() - это BufferedReader.

Если вы хотите использовать метод read(), то я бы сказал, что вам лучше использовать FileReader, который предназначен для этой цели. Вы можете read здесь, в учебниках по Java.

Так, Я думаю, ответ на ваш вопрос очень прост (не вдаваясь в бенчмаркинга и все, что explainations) -

  • Каждый read() обрабатывается базовой ОС и триггеры доступа к диску, сетевой активности , или некоторые другие операции, которые относительно дороги.
  • Когда вы используете readLine(), вы сохраняете все эти накладные расходы, поэтому readLine() всегда будет быстрее, чем read(), возможно, не для существенных данных, но быстрее.
+2

Как уже упоминалось в комментариях: Цель читателя «Буферизация» (!) Заключается в том, что он * буферизирует * некоторые данные. Таким образом, повторяющиеся 'read()' вызовы будут * not * заставлять байты считываться с диска по одному. Вместо этого он регулярно читает «куски» данных. Вы можете даже проследить его, чтобы увидеть, как в обоих, так и в режиме 'read' и' readLine', базовый 'FileReader' выполняет те же' read' вызовы, каждый из которых считывает 8192 байта. – Marco13

+0

@ Marco13 В этом посте есть много комментариев, и я даже не читал несколько, но я читал ответы. Если ваша точка в том, что 'read' также выполняет некоторую буферизацию, то я не уверен, однако я не могу исключить, что там может быть какая-то оптимизация, но все же основы остаются такими же в отношении классов BufferedReader и' FileReader' и почему 'read' медленнее, чем' readLine' - из-за большего количества входов/выходов. – hagrawal

+0

@hagrawal Вы на самом деле могли бы исключить это невероятно легко, просто проверив первый абзац документации (или быстро взглянув на код). Хотя само название, по-видимому, является мертвой поддачей - если считыватель * Buffered * Reader не читает буферы, что еще он может сделать? – Voo

0

Так что это практический ответ на мой собственный вопрос: Не используйте BufferedReader.read() использовать FileChannel вместо этого.(Очевидно, я не отвечаю на ПОЧЕМУ, что я записал в названии). Вот быстрый и грязный тест, надеемся, другие считают полезным:

@Test 
public void testFileChannel() throws IOException{ 

    FileChannel fileChannel = FileChannel.open(Paths.get("chr1.fa")); 
    long n= 0; 
    int noOfBytesRead = 0; 

    long t0= System.nanoTime(); 

    while(noOfBytesRead != -1){ 
     ByteBuffer buffer = ByteBuffer.allocate(10000); 
     noOfBytesRead = fileChannel.read(buffer); 
     buffer.flip(); 
     while (buffer.hasRemaining()) { 
      char x= (char)buffer.get(); 
      n++; 
     } 
    } 
    long t1= System.nanoTime(); 
    System.err.println((float)(t1-t0)/1e6); // ~ 250 ms 
    System.err.println("nchars: " + n); // 254235640 chars read 
} 

С ~ 250 мс для чтения весь символ файла по полукокса, эта стратегия значительно быстрее, чем BufferedReader.readLine() (~ 700 мс), не говоря уже read() , Добавление операторов if в цикле для проверки x == '\n' и x == '>' не имеет большого значения. Кроме того, установка StringBuilder для реконструкции линий не слишком сильно влияет на время. Так что это очень полезно для меня (по крайней мере пока).

Спасибо @ Marco13 за упоминание FileChannel.

0

Согласно документации:

Каждый read() вызов метода делает дорогой системный вызов.

Каждый вызов readLine() все еще делает дорогой системный вызов, однако, для большего количества байтов одновременно, поэтому вызовов меньше.

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

Смежные вопросы