我有一个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
?答案 0 :(得分:3)
调查此问题的正确方法是:
问:我是否以正确的方式使用ReversedLinesFileReader?
是。 (假设完全使用行阅读器是合适的。这取决于你真正想做的事情。例如,如果你只想向后计算行数,那么你应该在一个字符处读取1个字符。时间和计算换行序列。)
问:我在SSD上运行带有Ext4文件系统的Linux Mint。这可能与它有关吗?
可能。反向读取文件意味着操作系统用于提供快速I / O的预读策略可能不起作用。它可能与SSD的特性相互作用。
问:是不是应该从头到尾读取文件?
可能。见上文。
你没有考虑的另一件事是你的文件实际上可能包含一些非常长的行。瓶颈可以将字符组装成(长)行。
查看source code,当行非常长时,似乎存在O(N^2)
行为的可能性。关键部分是(我认为)"翻转"由FilePart
处理。注意"遗留的方式"数据被复制。
答案 1 :(得分:1)
您要求进行非常昂贵的操作。您不仅使用块中的随机访问来读取文件并向后移动(因此,如果文件系统正在读取,它正在读取错误的方向),您还要读取一个UTF-8的XML文件,并且编码是比固定字节编码慢。
然后最重要的是,你使用的效率不高。它在大小不方便的时候读取一个块(是否知道磁盘块大小?你是否设置了块大小以匹配你的文件系统?),同时处理编码并生成(不必要的?)部分字节数组的副本然后转把它变成一个字符串(你需要一个字符串来解析?)。它可以在没有副本的情况下创建字符串,并且真正创建字符串可能会延迟,并且您可以直接从缓冲区工作,只需要解码(例如,XML解析器也可以使用ByteArrays或缓冲区)。还有其他阵列副本不需要,但代码更方便。
它也可能有一个错误,它检查新行而不考虑如果实际上是多字节序列的一部分,该字符可能意味着不同的东西。它必须回顾一些额外的字符来检查这个可变长度编码,我不认为它是这样做的。
因此,您可以在文件系统上执行最快的事情,而不是一个很好的转发,只对文件进行大量缓冲的顺序读取。它应该至少读取多个磁盘块,以便它可以使用前向动量(将blockize设置为磁盘块大小的某个倍数将有所帮助),并且还可以避免在缓冲区边界处制作的“遗留”副本的数量。
可能有更快的方法。但它不会像以正向顺序读取文件那么快。
<强>更新强>
好的,所以我尝试了一个相当愚蠢的版本的实验,通过读取wikidata JSON转储中的前1000万行并反转这些行来处理大约27G的数据。
我的2015 Mac Book Pro上的计时(我的所有开发人员和许多镀铬窗口一直打开内存和一些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大小并尝试使用gzip压缩文件,也许是双线程模型,以便在处理前一个时使用下一个缓冲区gzipping。减少字符串分配,检查每个文件函数以确保没有任何额外的事情发生,确保没有双缓冲,等等。
丑陋但功能强大的代码是:
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 ")
}