2016-09-19 154 views
4

我有一个21.6GB的文件,我想从头到尾读取它,而不是像从前一样,从头到尾阅读。为什么ReversedLinesFileReader这么慢?

如果我使用下面的代码从头到尾读取文件的每一行,则需要1分12秒。

val startTime = System.currentTimeMillis() 
File("very-large-file.xml").forEachLine { 
    val i = 0 
} 
val diff = System.currentTimeMillis() - startTime 
println(diff.timeFormat()) 

现在,我已阅读,在反向读取文件,然后我应该使用ReversedLinesFileReader从Apache的百科全书。我创建了下面的扩展功能来做到这本:

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吗?
  • 我在SSD上运行带有Ext4文件系统的Linux Mint。这可能与它有什么关系?
  • 是不是应该从头到尾阅读文件?
+0

你知道什么是数据的模样?这条生产线有多统一? –

+0

@BrianKent,我不确定你的意思是如何统一换行符,但有问题的数据是来自OpenStreetMap项目的XML文件。 – Arthur

+0

您确定需要按相反顺序处理文件吗?我怀疑你会更好地阅读自然顺序中的行,并将大块数据保存到临时文件和/或数据库,然后以这种中间格式处理数据,如果数据块分成较小的文件和/或数据库可以然后轻松地按相反顺序处理。 – mfulton26

回答

1

您正在寻求非常昂贵的操作。您不仅在块中使用随机访问来读取文件并向后退出(因此,如果文件系统正在读取,它正在读取错误的方向),您还正在读取一个UTF-8 XML编码文件比固定字节编码慢。

然后最重要的是,你使用的算法效率不高。它在大小不便的时候读取块(是否可以识别磁盘块大小?是否将块大小设置为与文件系统匹配?),同时处理编码并使部分字节数组复制(不必要),然后转动它变成一个字符串(你需要一个字符串解析?)。它可以创建没有副本的字符串,并且真正创建该字符串可能会被延迟,并且如果需要(例如,XML解析器也可以从ByteArrays或缓冲区运行),则直接从缓冲区解码。还有其他的阵列副本不需要,但对代码更方便。

它也可能有一个错误,它会检查换行符,而不考虑如果实际上是多字节序列的一部分字符可能意味着不同的东西。它将不得不回顾一些额外的字符来检查这个可变长度编码,我没有看到它这样做。

因此,不是一个很好的向前缓冲文件的顺序读取,这是文件系统上可以做的最快的事情,而是一次随机读取1个块。它至少应该读取多个磁盘块,以便能够使用前向动量(将块大小设置为磁盘块大小的一些倍数将有所帮助),并且还可以避免在缓冲区边界处制作的“剩余”副本的数量。

有可能是更快的方法。但它不会像按照顺序读取文件一样快。

UPDATE:

好了,我试过的实验包含通过读取维基数据JSON转储前10万行和扭转这些线路围绕数据的27G处理一个相当愚蠢的版本。

我的2015 Mac Book Pro计时器(我的所有开发工具和许多chrome窗口一直开着进食内存和一些CPU,大约5G的内存都是免费的,虚拟机大小是默认设置,根本没有设置参数,不在调试器中运行):

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文件,也可能是一个双线程模型,在处理前一个文件时有下一个缓冲区。更少的字符串分配,检查每个文件函数以确保没有额外的事情发生,确保没有双缓冲,等等。

丑陋,但功能代码:

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 ") 
} 
2

探讨这个问题的正确方法应该是:

  1. 纯Java编写一个版本的测试。
  2. 基准测试,以确保性能问题仍然存在。
  3. 将其分析以找出性能瓶颈所在。

问:我在正确的方式使用ReversedLinesFileReader?

是的。 (假设使用线阅读器是一件合适的事情,这取决于你真正想要做什么,例如,如果你只是想向后数线,那么你应该阅读1个字符时间和计算新行序列。)

问:我正在SSD上运行带有Ext4文件系统的Linux Mint。这可能与它有什么关系?

可能。反向读取文件意味着操作系统为提供快速I/O而使用的预读策略可能不起作用。它可能会与SSD的特性相互作用。

问:是不是应该从头到尾读取文件?

可能。往上看。


另一件你没有考虑的事情是你的文件实际上可能包含一些非常长的行。瓶颈可能是将字符组合成(长)行。

看看source code,看起来线路很长时,有可能存在O(N^2)行为。关键部分是(我认为)“翻转”由FilePart处理。请注意复制“剩余”数据的方式。