【问题标题】:Java while loop dramatically slows down over time after a large number of iterationsJava while循环在大量迭代后随着时间的推移显着减慢
【发布时间】:2015-07-20 14:52:58
【问题描述】:

我的程序在 while 循环中逐行读取文本文件。然后它处理每一行并提取一些要写入输出的信息。它在 while 循环中所做的一切都是 O(1),除了我认为是 O(N) 的两个 ArrayList indexOf() 方法调用。该程序在开始时以合理的速度(每 100 秒 100 万行)运行,但随着时间的推移,它会显着减慢。我在输入文件中有 70 M 行,因此循环迭代了 7000 万次。理论上这应该需要大约 2 个小时,但实际上需要 13 个小时。问题出在哪里?

这里是sn-p的代码:

BufferedReader corpus = new BufferedReader(
            new InputStreamReader(
                        new FileInputStream("MyCorpus.txt"),"UTF8"));

Writer outputFile = new BufferedWriter(new OutputStreamWriter(
            new FileOutputStream("output.txt"), "UTF-8"));

List<String> words = new ArrayList();
//words is being updated with relevant values here   

LinkedHashMap<String,Integer> DIC = new LinkedHashMap();
//DIC is being updated with relevant key-value pairs here    

String line = ""; 
while ((line = corpus.readLine()) != null)
    String[] parts = line.split(" ");
    if (DIC.containsKey(parts[0]) && DIC.containsKey(parts[1])) {

        int firstIndexPlusOne = words.indexOf(parts[0])+ 1;
        int secondIndexPlusOne = words.indexOf(parts[1]) +1;

        outputFile.write(firstIndexPlusOne +" "+secondIndexPlusOne+" "+parts[2]+"\n");
        } else { 
            notFound++;
            outputFile.write("NULL\n");
        }
    }
outputFile.close();

【问题讨论】:

  • 还有很多优化的空间——如果你知道parts总是一个二元素数组,你应该使用indexOf(' ')并用substring分割,所以你有两个变量@987654325 @ 和 part2。 - EDIT:忽略了parts[2] - 但它也适用于三个元素。
  • 语料库有 70 M 行,每行包含三个单词。刚刚更新了代码。
  • 我会说它取决于——除了 JVM 运行时优化魔法、垃圾收集器等等——取决于实际的文件内容?例如,如果在前 10M 行中 containsKey 永远不会为真,但对于剩余的 60M 行它评估为真,那么整个文件写入业务才执行,这需要大量时间。 Big-O-considerations 在这里可能没什么帮助,因为甚至涉及到 I/O。也许您可以包含一个计数器来计算和/或输出针对文件的哪些行执行了哪些代码行?
  • 我想你会发现执行 I/O 消耗了 99%(可能更多)的运行时,所以归结为执行文件写入的时间,这完全取决于文件的内容。
  • 你确定这就是全部代码吗? “words”和“DIC”似乎都没有更新。

标签: java while-loop time-complexity


【解决方案1】:

我假设你在你去wordsArrayList时添加单词。

您正确地指出words.indexOfO(N),这就是您的问题的原因。随着N 的增加(您将单词添加到列表中),这些操作需要的时间越来越长。

为避免这种情况,请对列表进行排序并使用binarySearch

要保持排序,请在每个单词上使用binarySearch 来确定插入位置。这会将您的复杂性从O(n) 提高到O(log(N))

【讨论】:

  • 评论//words is being updated with relevant values here 暗示words 是在循环运行之前预先构建的,然后没有在循环内更新,这不是一个可能的原因吗?将不断变化的集合的索引写入结果文件有什么意义?毕竟,获取这些索引号似乎是整个用例?当然,这只是我的解释,只有@Meghi 可以肯定地回答这个问题。
  • stef77 是对的。单词正在循环外更新,之后不会更改。我认为您是对的,主要问题在于 indexOf() 方法。据我所见,即使 I/O 也不那么耗时。所以你建议我在使用二进制搜索的排序列表上编写自己的 indexOf 方法?单词包含字符串值。我想知道二进制搜索如何工作。
  • @Meghi - 好的,所以排序吧。 binarySearch 假定列表已排序。只需在构建列表后对列表进行排序并使用binarySearch 而不是indexof - 它返回列表中的位置,就像indexof 一样。
  • @OldCurmudgeon - 我喜欢这个主意。但除了这种或任何其他类型的优化之外,问题是为什么程序(现在的方式)会随着时间的推移而变慢?
【解决方案2】:

我认为,words 是为了收集独特的单词,因此使用 Set。

Set<String> words = new HashSet<>();
Map<String, Integer> DIC = new HashMap<>();

DIC 也类似于频率表,在这种情况下,dic.keySet()words 相同。 LinkedHashMap 维护一个额外的列表,以保持条目按插入顺序排序。

编写单独的字符串,而不是首先创建新字符串更快。

   outputFile.write(firstIndexPlusOne);
   outputFile.write(" ");
   outputFile.write(secondIndexPlusOne);
   outputFile.write(" ");
   outputFile.write(parts[2]);
   outputFile.write("\n");

【讨论】:

  • OP 需要words.indexOf(parts[0]),因此将其设为Set 可能行不通,但LinkedHashSet 可能是一种选择。
  • @OldCurmudgeon indexOf 也很痛苦,可能不需要,也许一些哈希图就可以了;第二个循环来创建数字索引等。
  • @Joop Eggen 我测试了这个,看起来像这样写有点慢。
  • @Meghi 对此感到抱歉;我猜是热点?编译器做得更好。谢谢你的智慧。
【解决方案3】:

我认为你的问题之一是那一行:

outputFile.write(firstIndexPlusOne +" "+secondIndexPlusOne+" "+parts[2]+"\n");

由于字符串是不可变的,因此您会弄乱内存。另外,也许尝试在循环中的每一轮刷新写入缓冲区,它可能会有所改善(我的假设在这里)

尝试类似:

    String line = ""; 
    StringBuilder sb = new StringBuilder();
    while ...
    ...
      sb.append(firstIndexPlusOne);
      sb.append(" ");
      sb.append(secondIndexPlusOne);
      sb.append(" ");
      sb.append(parts[2]);
      sb.append("\n");
      outputFile.write(sb.toString());
      sb.setLength(0);
      outputFile.flush();

另外,也许值得一读:Tuning Java I/O Performance (Oracle)

【讨论】:

  • 这使得执行速度变慢了。
  • 这可能是由于刷新,可能触发了真正的 I/O,这可以解释性能下降的原因。顺便说一句,我想我在某处读过(对不起,现在找不到)编译器(javac,不是在运行时)实际上将这样的字符串连接编译为现在使用 StringBuffers,所以这两个代码的字节码无论如何,sn-ps 可能看起来一样。
  • 我不是 Java 开发人员,所以这可能是不正确的,但为什么不直接将每个组件写入编写器,如下所示:outputFile.write(firstIndexPlusOne.toString());outputFile.write(" "); 等?似乎根本不需要构造StringBuilder
【解决方案4】:

如果语料库和单词列表都已排序,则 words.indexOf(..) 调用执行的线性搜索在每次迭代中都会变慢。

在处理语料库之前从您的单词列表中构建一个 HashMap(..) 可以解决问题。这样做可能是一个优化的好主意,即使这不是问题。

【讨论】:

  • 不,它们没有排序。随着时间的推移,它仍然会变慢。
【解决方案5】:

假设您在循环中既不更新words 也不更新DIC,显然当DIC.containsKey(parts[0]) &amp;&amp; DIC.containsKey(parts[1]) 评估为真时消耗了最多的运行时间。

如果您的问题是“为什么会变慢”,而不是“我怎样才能加快速度”,我建议您将文件的前 10M 行复制到另一个文件中并复制它们以便您收到 70M 行,其中包含前 10M 行的副本。然后,执行您的代码。如果即使一遍又一遍地检查相同的内容,它也会变慢,您可以检查有关字符串构建器等的其他答案。

如果您没有遇到变慢的情况,那么显然这取决于您的 70M 文件的实际内容。很可能,对于原始文件的剩余 60M 行,DIC.containsKey(parts[0]) &amp;&amp; DIC.containsKey(parts[1]) 的计算结果为 true 的频率更高,因此内部循环执行的频率更高,需要更多的时间。

在后一种情况下,我怀疑您是否可以通过应用单次写入来欺骗 I/O 负载,从而获得性能提升,但当然我可能错了。你得试试。但首先,我建议探索问题的根源,我认为这在于文件内容的结构。在了解了代码相对于给定输入的执行情况后,您可以尝试优化(尽管我会尝试将整个字符串保存在内存中,并在循环后的一个操作中写入其内容,而不是执行很多小的写入操作) .

【讨论】:

  • 据我所知, containsKey 命中的数量或多或少均匀分布在整个输入文件中。我的意思是,例如,第五个 10M 中的点击量并不比前 10M 中的更多。而且,只有7%的输入数据没有通过DIC.containsKey(parts[0]) &amp;&amp; DIC.containsKey(parts[1])
  • 在我看来,现在最可能的原因是 BufferedWriter 在一段时间后开始真正的 I/O,从而严重降低了性能,或者在“慢”60M 部分找到的单词更接近结尾数组的数量比它们在第一个“快”10M。因此, indexOf 需要更长的时间。如果您将前 10M 多次复制到一个新文件中并查看它是否更快,则可以发现这一点。当然,如果是由于 I/O,您不会注意到差异...但是索引已写入您的结果文件中,当评估慢 60M 时,它们是否可能更大?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
  • 2017-11-02
  • 2014-07-28
  • 1970-01-01
  • 2014-02-04
  • 2018-09-10
  • 1970-01-01
相关资源
最近更新 更多