Java, почему чтение из MappedByteBuffer медленнее, чем чтение из BufferedReader
Я пытался прочитать строки из файла, который может быть большим.
Чтобы улучшить производительность, я попытался использовать сопоставленный файл. Но когда я сравниваю производительность, я обнаруживаю, что путь к отображаемому файлу даже немного медленнее, чем при чтении из BufferedReader
public long chunkMappedFile(String filePath, int trunkSize) throws IOException {
long begin = System.currentTimeMillis();
logger.info("Processing imei file, mapped file [{}], trunk size = {} ", filePath, trunkSize);
//Create file object
File file = new File(filePath);
//Get file channel in readonly mode
FileChannel fileChannel = new RandomAccessFile(file, "r").getChannel();
long positionStart = 0;
StringBuilder line = new StringBuilder();
long lineCnt = 0;
while(positionStart < fileChannel.size()) {
long mapSize = positionStart + trunkSize < fileChannel.size() ? trunkSize : fileChannel.size() - positionStart ;
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, positionStart, mapSize);//mapped read
for (int i = 0; i < buffer.limit(); i++) {
char c = (char) buffer.get();
//System.out.print(c); //Print the content of file
if ('\n' != c) {
line.append(c);
} else {// line ends
processor.processLine(line.toString());
if (++lineCnt % 100000 ==0) {
try {
logger.info("mappedfile processed {} lines already, sleep 1ms", lineCnt);
Thread.sleep(1);
} catch (InterruptedException e) {}
}
line = new StringBuilder();
}
}
closeDirectBuffer(buffer);
positionStart = positionStart + buffer.limit();
}
long end = System.currentTimeMillis();
logger.info("chunkMappedFile {} , trunkSize: {}, cost : {} " ,filePath, trunkSize, end - begin);
return lineCnt;
}
public long normalFileRead(String filePath) throws IOException {
long begin = System.currentTimeMillis();
logger.info("Processing imei file, Normal read file [{}] ", filePath);
long lineCnt = 0;
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
processor.processLine(line.toString());
if (++lineCnt % 100000 ==0) {
try {
logger.info("file processed {} lines already, sleep 1ms", lineCnt);
Thread.sleep(1);
} catch (InterruptedException e) {}
} }
}
long end = System.currentTimeMillis();
logger.info("normalFileRead {} , cost : {} " ,filePath, end - begin);
return lineCnt;
}
Результат теста в Linux с чтением файла размером 537 МБ:
MappedBuffer way:
2017-09-28 14:33:19.277 [main] INFO com.oppo.push.ts.dispatcher.imei2device.ImeiTransformerOfflineImpl - process imei file ends:/push/file/imei2device-local/20170928/imei2device-13 , lines :12758858 , cost :14804 , lines per seconds: 861852.0670089165
Способ BufferedReader:
2017-09-28 14:27:03.374 [main] INFO com.oppo.push.ts.dispatcher.imei2device.ImeiTransformerOfflineImpl - process imei file ends:/push/file/imei2device-local/20170928/imei2device-13 , lines :12758858 , cost :13001 , lines per seconds: 981375.1249903854
3 ответа
В том-то и дело: файловый ввод-вывод не так прост и прост.
Вы должны иметь в виду, что ваша операционная система оказывает огромное влияние на то, что именно произойдет. В этом смысле: не существует надежных правил, которые работали бы для всех реализаций JVM на всех платформах.
Когда вам действительно нужно беспокоиться о последнем уровне производительности, углубленное профилирование на целевой платформе является основным решением.
Кроме того, вы неправильно понимаете этот аспект "производительности". Значение: IO с отображением в памяти волшебным образом не увеличивает производительность чтения одного файла в приложении один раз. Его основные преимущества идут по этому пути:
mmap отлично подходит, если у вас есть несколько процессов, обращающихся к данным только для чтения из одного и того же файла, что часто встречается в тех серверных системах, которые я пишу. mmap позволяет всем этим процессам совместно использовать одни и те же страницы физической памяти, экономя много памяти.
(цитата из этого ответа на использование C mmap()
системный вызов)
Другими словами: ваш пример о чтении содержимого файла. В конце концов, ОС все равно придется обращаться к диску, чтобы прочитать все байты оттуда. Значение: он читает содержимое диска и помещает его в память. Когда вы делаете это в первый раз... действительно не имеет значения, что вы делаете некоторые "особые" вещи в дополнение к этому. Наоборот - когда вы делаете "особые" вещи, подход с отображением памяти может быть даже медленнее - из-за накладных расходов по сравнению с "обычным" чтением.
И вернемся к моей первой записи: даже если у вас будет 5 процессов, читающих один и тот же файл, подход с отображением в памяти не обязательно быстрее. Как может понять Linux: я уже прочитал этот файл в память, и он не изменился - поэтому даже без явного "отображения памяти" ядро Linux может кешировать информацию.
Отображение памяти на самом деле не дает никаких преимуществ, поскольку, несмотря на то, что вы загружаете файл в память, вы все равно обрабатываете его по одному байту за раз. Вы можете увидеть увеличение производительности, если обработаете буфер подходящего размера byte[]
ломти. Даже тогда BufferedReader
Версия может работать лучше или, по крайней мере, почти так же.
Суть вашей задачи заключается в последовательной обработке файла. BufferedReader
уже делает это очень хорошо, и код прост, поэтому, если бы мне пришлось выбирать, я бы выбрал самый простой вариант.
Также обратите внимание, что ваш буферный код не работает, за исключением однобайтовых кодировок. Как только вы получите несколько байтов на символ, он великолепно потерпит неудачу.
GhostCat это правильно. И в дополнение к вашему выбору ОС, другие вещи, которые могут повлиять на производительность.
Отображение файла приведет к большей потребности в физической памяти. Если физическая память "ограничена", это может вызвать активность подкачки и снижение производительности.
ОС может использовать другую стратегию упреждающего чтения, если вы читаете файл, используя
read
Системные вызовы против отображения в памяти. Опережающее чтение (в буферный кеш) может значительно ускорить чтение файлов.Размер буфера по умолчанию для
BufferedReader
и размер страницы памяти ОС, вероятно, будет другим. Это может привести к тому, что размер запросов на чтение диска будет другим. (Большее чтение часто приводит к большей пропускной способности ввода-вывода. По крайней мере, до определенной точки.)
Также могут быть "артефакты", вызванные тем, как вы тестируете. Например:
- При первом чтении файла копия некоторых или всех файлов будет помещена в буферный кеш (в память).
- Во второй раз, когда вы читаете тот же файл, его части могут все еще находиться в памяти, и
read
время будет короче.