您要求进行非常昂贵的手术。您不仅在块中使用随机访问来读取文件并向后读取(因此,如果文件系统正在向前读取,则它正在读取错误的方向),您还在读取一个 UTF-8 的 XML 文件,并且编码是比固定字节编码慢。
除此之外,您使用的算法效率较低。它在处理编码和制作(不必要的?)部分字节数组的副本时向后读取一个块(它是否知道磁盘块大小?您是否设置块大小以匹配您的文件系统?)它变成一个字符串(你需要一个字符串来解析吗?)。它可以在没有副本的情况下创建字符串,并且真正创建字符串可能会被推迟,并且您可以直接从缓冲区工作,仅在需要时才解码(例如 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 大小,也可以尝试使用 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 ")
}