Почему производительность BufferedReader намного хуже, чем BufferedInputStream?

Я понимаю, что использование BufferedReader (обертка FileReader) будет значительно медленнее, чем использование BufferedInputStream (обертка FileInputStream), потому что необработанные байты должны быть преобразованы в символы. Но я не понимаю, почему это так медленнее! Вот два примера кода, которые я использую:

BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(filename));
try {
  byte[] byteBuffer = new byte[bufferSize];
  int numberOfBytes;
  do {
    numberOfBytes = inputStream.read(byteBuffer, 0, bufferSize);
  } while (numberOfBytes >= 0);
}
finally {
  inputStream.close();
}

а также:

BufferedReader reader = new BufferedReader(new FileReader(filename), bufferSize);
try {
  char[] charBuffer = new char[bufferSize];
  int numberOfChars;
  do {
    numberOfChars = reader.read(charBuffer, 0, bufferSize);
  } while (numberOfChars >= 0);
}
finally {
  reader.close();
}

Я пробовал тесты с использованием различных размеров буфера, все с 150-мегабайтным файлом. Вот результаты (размер буфера в байтах, время в миллисекундах):

Buffer   Input
  Size  Stream  Reader
 4,096    145     497
 8,192    125     465
16,384     95     515
32,768     74     506
65,536     64     531

Как видно, самое быстрое время для BufferedInputStream (64 мс) в семь раз быстрее, чем самое быстрое время для BufferedReader (465 мс). Как я уже говорил выше, у меня нет проблемы со значительной разницей; но такая большая разница просто кажется неразумной.

Мой вопрос: есть ли у кого-нибудь предложения по улучшению производительности BufferedReader или альтернативный механизм?


person Andy King    schedule 13.01.2013    source источник
comment
Я думаю, что наиболее вероятным объяснением является то, что ваш тест ошибочен; например вы не учитываете должным образом эффекты прогрева JVM. Пожалуйста, опубликуйте полную вещь.   -  person Stephen C    schedule 13.01.2013
comment
@StephenC или, может быть, кеш диска?   -  person John Dvorak    schedule 13.01.2013
comment
Вы сравниваете яблоки и апельсины — второй тест включает преобразование байтов в char, чего не делает первый. Если вам нужны данные char, используйте Reader; если вам нужны байты, используйте InputStream. Я думаю, вы обнаружите, что быстрее всего будет BufferedReader обернуть InputStreamReader обернуть BufferedInputStream обернуть FileInputStream. Также взгляните на эту тему на как написать бенчмарк.   -  person Ted Hopp    schedule 13.01.2013
comment
Результат также может зависеть от используемой кодировки символов.   -  person Henry    schedule 13.01.2013
comment
@StephenC Я не утверждаю, что мой тест очень научный, но я не думаю, что разница является результатом запуска JVM, выполнения GC или чего-то в этом роде ... Я запустил код в циклах и взял среднее значение время на гораздо большей выборке; также оба теста запускались в одной и той же JVM (бывает, что сначала выполняется BufferedInputStream, но это, похоже, не важно). Пожалуйста, объясните, почему вы считаете, что сроки ошибочны.   -  person Andy King    schedule 13.01.2013
comment
Не видя вашего фактического кода, я не могу дать вам полное объяснение. Но мои основные причины так думать: 1) время, о котором вы сообщаете, кажется мне неправдоподобным, и 2) вы не ответили на теорию прогрева JVM... что предполагает, что вы не понимаете ее значения. Просто опубликуйте код... чтобы мы могли увидеть, что вы на самом деле делаете, и попытаться воспроизвести это.   -  person Stephen C    schedule 13.01.2013
comment
@Jan Dvorak Даже если задействовано кэширование диска, я не думаю, что это имеет какое-либо значение ... как я уже говорил в предыдущем комментарии, код для BufferedInputStream выполняется в том же выполнении, что и код для BufferedReader. Я на самом деле не думаю, что файл размером 150 МБ кэшируется, но, возможно, это так ... но как это объясняет разницу во времени между обработкой символов и байтов?   -  person Andy King    schedule 13.01.2013
comment
@TedHopp Да, как я пытался объяснить в своем вопросе, я понимаю, что существует значительная разница между обработкой необработанных байтов и символов. Просто кажется, что семикратная разница в производительности больше, чем я ожидал. И у меня есть ощущение, что ваше предложение обернуть FileInputStream в три слоя несерьезно ... если это так, просто дайте мне знать, и я попробую!   -  person Andy King    schedule 13.01.2013
comment
Предложение было совершенно серьезным. Некоторое время назад я провел несколько экспериментов и был удивлен результатами. Это улучшение второго порядка, но, похоже, оно определенно есть.   -  person Ted Hopp    schedule 13.01.2013
comment
Одинаков ли размер буфера в обоих случаях? В байтах, а не в абсолютном значении? Вы выполняете оба теста в одной и той же JVM? И если да, то в каком порядке? Вы пробовали разные аргументы размера при построении BufferedInputStream/Reader?   -  person user207421    schedule 13.01.2013
comment
@EJP Величина размера буферов была одинаковой, но, следовательно, не физический размер ... массив символов использует размер символа (я думаю, что на моей машине это четыре байта), а массив байтов использует байт. Этим можно объяснить различия в относительных скоростях при изменении размера буфера. Оба теста выполняются одним и тем же методом в одном и том же выполнении программы (и моя тестовая система запускает их более одного раза).   -  person Andy King    schedule 13.01.2013
comment
@StephenC Спасибо за ваши комментарии ... почему вы думаете, что времена неправдоподобны? И почему вы думаете, что на это может повлиять прогрев JVM? Тесты выполняются в одной и той же JVM, при одном и том же выполнении программы, более быстрый тест выполняется первым (я ожидаю, что прогрев JVM приведет к тому, что более ранний тест будет медленнее). Когда я меняю порядок тестов, я не вижу разницы во времени. Единственная причина, по которой я не решаюсь публиковать код, заключается в том, что это небольшая часть более крупной программы... Думаю, я мог бы выделить ее в отдельной публикации. Вы можете попробовать код, который я разместил.   -  person Andy King    schedule 13.01.2013
comment
@TedHopp Я попробовал подход BufferedReader+InputStreamReader+BufferedInputStream+FileInputStream, и результаты были в пределах нескольких миллисекунд от простого теста BufferedReader+FileReader (как для маленьких, так и для больших буферов).   -  person Andy King    schedule 13.01.2013
comment
Хм. Я думаю, мне нужно пересмотреть мое тестирование.   -  person Ted Hopp    schedule 13.01.2013


Ответы (2)


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

byte[] bytes = new byte[150 * 1024 * 1024];
Arrays.fill(bytes, (byte) '\n');

for (int i = 0; i < 10; i++) {
    long start = System.nanoTime();
    StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes));
    long time = System.nanoTime() - start;
    System.out.printf("Time to decode %,d MB was %,d ms%n",
            bytes.length / 1024 / 1024, time / 1000000);
}

отпечатки

Time to decode 150 MB was 226 ms
Time to decode 150 MB was 167 ms

ПРИМЕЧАНИЕ. Необходимость выполнять это в сочетании с системными вызовами может замедлить обе операции (поскольку системные вызовы могут нарушить работу кеша).

person Peter Lawrey    schedule 13.01.2013

в реализации BufferedReader есть фиксированная константа defaultExpectedLineLength = 80, которая используется в методе readLine при выделении StringBuffer. Если у вас есть большой файл с большим количеством строк длиннее 80, этот фрагмент может быть чем-то, что можно улучшить.

if (s == null) 
    s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
person Jakub C    schedule 21.10.2014