Вы просите очень дорогую операцию. Вы не только используете произвольный доступ в блоках для чтения файла и возвращаетесь назад (поэтому, если файловая система читает вперед, она читает в неправильном направлении), вы также читаете файл XML, который является UTF-8, и кодировка медленнее, чем кодирование с фиксированным байтом.
Кроме того, вы используете менее эффективный алгоритм. Он читает блок в момент неудобного размера (знает ли он размер блока диска? Вы устанавливаете размер блока в соответствии с вашей файловой системой?) в обратном направлении при обработке кодирования и делает (ненужную?) копию частичного массива байтов, а затем поворачивает в строку (вам нужна строка для разбора?). Он может создать строку без копии, и действительное создание строки, вероятно, может быть отложено, и вы работаете непосредственно с декодированием буфера, если вам нужно (парсеры XML, например, также работают с ByteArrays или буферами). И есть другие копии массива, которые просто не нужны, но для кода удобнее.
У него также может быть ошибка в том, что он проверяет новые строки, не учитывая, что символ может означать что-то другое, если он на самом деле является частью многобайтовой последовательности. Чтобы проверить это на наличие кодировок переменной длины, ему пришлось бы оглянуться назад на несколько дополнительных символов, я не вижу, чтобы он это делал.
Таким образом, вместо последовательного чтения файла с хорошей буферизацией только вперед, которое является самой быстрой вещью, которую вы можете сделать в своей файловой системе, вы выполняете случайное чтение по 1 блоку за раз. Он должен, по крайней мере, читать несколько дисковых блоков, чтобы он мог использовать импульс вперед (поможет установить размер блока, кратный размеру вашего дискового блока), а также избежать количества «оставшихся» копий, сделанных на границах буфера.
Вероятно, есть более быстрые подходы. Но это будет не так быстро, как чтение файла в прямом порядке.
ОБНОВЛЕНИЕ:
Хорошо, поэтому я провел эксперимент с довольно глупой версией, которая обрабатывает около 27 ГБ данных, читая первые 10 миллионов строк из дампа викиданных JSON и обращая эти строки.
Тайминги на моем Mac Book Pro 2015 года (со всеми моими разработчиками и многими открытыми хромированными окнами, которые постоянно потребляют память и часть процессора, около 5 ГБ общей памяти свободно, размер виртуальной машины по умолчанию без каких-либо параметров, не работает под отладчиком ):
reading in reverse order: 244,648 ms = 244 secs = 4 min 4 secs
reading in forward order: 77,564 ms = 77 secs = 1 min 17 secs
temp file count: 201
approx char count: 29,483,478,770 (line content not including line endings)
total line count: 10,050,000
Алгоритм состоит в том, чтобы читать исходный файл по строкам, буферизуя по 50000 строк за раз, записывая строки в обратном порядке в пронумерованный временный файл. Затем, после того, как все файлы записаны, они считываются в обратном порядке номеров вперед по строкам. В основном разделяя их на фрагменты в обратном порядке сортировки оригинала. Его можно оптимизировать, потому что это самая наивная версия этого алгоритма без настройки. Но он делает то, что файловые системы делают лучше всего, последовательное чтение и последовательную запись с буферами хорошего размера.
Так что это намного быстрее, чем тот, который вы использовали, и его можно настроить отсюда, чтобы он был более эффективным. Вы можете обменять ЦП на размер дискового ввода-вывода и попробовать также использовать gzip-файлы, возможно, двухпоточную модель, чтобы иметь следующий буфер gzip при обработке предыдущего. Меньше выделений строк, проверка каждой файловой функции, чтобы убедиться, что ничего лишнего не происходит, отсутствие двойной буферизации и многое другое.
Уродливый, но функциональный код:
package com.stackoverflow.reversefile
import java.io.File
import java.util.*
fun main(args: Array<String>) {
val maxBufferSize = 50000
val lineBuffer = ArrayList<String>(maxBufferSize)
val tempFiles = ArrayList<File>()
val originalFile = File("/data/wikidata/20150629.json")
val tempFilePrefix = "/data/wikidata/temp/temp"
val maxLines = 10000000
var approxCharCount: Long = 0
var tempFileCount = 0
var lineCount = 0
val startTime = System.currentTimeMillis()
println("Writing reversed partial files...")
try {
fun flush() {
val bufferSize = lineBuffer.size
if (bufferSize > 0) {
lineCount += bufferSize
tempFileCount++
File("$tempFilePrefix-$tempFileCount").apply {
bufferedWriter().use { writer ->
((bufferSize - 1) downTo 0).forEach { idx ->
writer.write(lineBuffer[idx])
writer.newLine()
}
}
tempFiles.add(this)
}
lineBuffer.clear()
}
println(" flushed at $lineCount lines")
}
// read and break into backword sorted chunks
originalFile.bufferedReader(bufferSize = 4096 * 32)
.lineSequence()
.takeWhile { lineCount <= maxLines }.forEach { line ->
lineBuffer.add(line)
if (lineBuffer.size >= maxBufferSize) flush()
}
flush()
// read backword sorted chunks backwards
println("Reading reversed lines ...")
tempFiles.reversed().forEach { tempFile ->
tempFile.bufferedReader(bufferSize = 4096 * 32).lineSequence()
.forEach { line ->
approxCharCount += line.length
// a line has been read here
}
println(" file $tempFile current char total $approxCharCount")
}
} finally {
tempFiles.forEach { it.delete() }
}
val elapsed = System.currentTimeMillis() - startTime
println("temp file count: $tempFileCount")
println("approx char count: $approxCharCount")
println("total line count: $lineCount")
println()
println("Elapsed: ${elapsed}ms ${elapsed / 1000}secs ${elapsed / 1000 / 60}min ")
println("reading original file again:")
val againStartTime = System.currentTimeMillis()
var againLineCount = 0
originalFile.bufferedReader(bufferSize = 4096 * 32)
.lineSequence()
.takeWhile { againLineCount <= maxLines }
.forEach { againLineCount++ }
val againElapsed = System.currentTimeMillis() - againStartTime
println("Elapsed: ${againElapsed}ms ${againElapsed / 1000}secs ${againElapsed / 1000 / 60}min ")
}
person
Jayson Minard
schedule
20.09.2016