我有一个21.6GB的文件,我想从头开始读取它,而不是像通常那样从头开始读取。
如果我使用以下代码从头至尾读取文件的每一行,则需要1分12秒。
val startTime = System.currentTimeMillis()
File("very-large-file.xml").forEachLine {
val i = 0
}
val diff = System.currentTimeMillis() - startTime
println(diff.timeFormat())
现在,我已经阅读了要反向读取的文件,然后应该使用Apache Commons中的
ReversedLinesFileReader
。我已经创建了以下扩展功能来做到这一点:fun File.forEachLineFromTheEndOfFile(action: (line: String) -> Unit) {
val reader = ReversedLinesFileReader(this, Charset.defaultCharset())
var line = reader.readLine()
while (line != null) {
action.invoke(line)
line = reader.readLine()
}
reader.close()
}
然后以以下方式调用它,这与以前的方式相同,只是调用了
forEachLineFromTheEndOfFile
函数:val startTime = System.currentTimeMillis()
File("very-large-file.xml").forEachLineFromTheEndOfFile {
val i = 0
}
val diff = System.currentTimeMillis() - startTime
println(diff.timeFormat())
这需要 17分钟50秒来运行!
ReversedLinesFileReader
? 最佳答案
您正在要求非常昂贵的手术。您不仅会在块中使用随机访问来读取文件并向后移动(因此,如果文件系统正在向前读取,则会读取错误的方向),而且您还在读取一个XML文件,它是UTF-8,编码是比固定字节编码慢。
然后最重要的是,您使用的是效率不高的算法。它会在处理编码时向后读取大小不方便的块(是否知道磁盘块大小?是否将块大小设置为与文件系统匹配?),然后复制(不必要?)部分字节数组的副本,然后旋转将其转换为字符串(您需要解析字符串吗?)。它可以创建没有副本的字符串,并且实际上可以推迟创建字符串,如果需要,您可以直接从缓冲区直接解码(例如,XML解析器也可以从ByteArrays或缓冲区工作)。还有其他一些数组副本,这些副本是不需要的,但是对于代码来说更方便。
它还可能有一个错误,即它检查换行符,而不考虑如果字符实际上是多字节序列的一部分,则该字符可能意味着不同的含义。它必须回顾一些额外的字符才能检查此变量的长度编码,但我不认为这样做。
因此,您不是一次只对文件进行重缓冲的顺序读取,而是在文件系统上执行最快的操作,而是一次随机读取1个块。它至少应读取多个磁盘块,以便可以使用正向动量(将块大小设置为磁盘块大小的几倍会有所帮助),并且还应避免在缓冲区边界处制作“剩余”副本。
可能有更快的方法。但这不如向前读取文件那么快。
更新:
好的,所以我尝试了一个相当愚蠢的版本的实验,该版本通过从wikidata JSON转储中读取前1000万行并将这些行反转来处理大约27G的数据。
我的2015 Mac Book Pro上的时间(所有开发人员的东西和许多chrome窗口始终打开并占用内存和一些CPU,约5G的总内存可用,VM大小默认为未设置任何参数,不在调试器下运行):
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行的行读取原始文件,然后以相反的顺序将行写入编号的临时文件。然后,在写入所有文件之后,将以相反的数字顺序逐行读取它们。基本上将它们分为原始的反向排序顺序片段。可以对其进行优化,因为这是该算法最幼稚的版本,无需进行任何调整。但是,它确实执行了文件系统最擅长的事情,即具有适当大小的缓冲区的顺序读取和顺序写入。
因此,这比您所使用的要快得多,并且可以从此处进行调整以提高效率。您可以将CPU换成磁盘I/O大小,并尝试使用压缩文件,也许是两线程模型,以便在处理前一个缓冲区时压缩下一个缓冲区。更少的字符串分配,检查每个文件功能以确保没有其他事情发生,确保没有双缓冲,等等。
丑陋但实用的代码是:
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 ")
}
关于java - 为什么ReversedLinesFileReader这么慢?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39582014/