【问题标题】:Can Java compiler optimize adding to a set in recursive methodsJava编译器可以优化添加到递归方法中的集合吗
【发布时间】:2014-06-02 10:25:14
【问题描述】:

简单的问题主要是出于对 Java 编译器足够聪明可以做什么的好奇。我知道并非所有编译器都是平等构建的,但我想知道其他人是否认为期望对我可能运行的大多数编译器进行优化是合理的,而不是它是否适用于特定版本或所有版本。

假设我有一些树结构,我想收集一个节点的所有后代。有两种简单的递归方法。

对我来说,更自然的方法是这样的:

public Set<Node> getDescendants(){

   Set<Node> descendants=new HashSet<Node>();
   descendants.addall(getChildren());

   for(Node child: getChildren()){
      descendants.addall(child.getDescendants());
   }

   return descendants;
}

但是,假设没有编译器优化和大小合适的树,这可能会变得相当昂贵。在每次递归调用中,我创建并完全填充一个集合,只返回那个设置堆栈,以便调用方法可以将我返回集合的内容添加到后代集合的 it's 版本中,丢弃该版本刚刚在递归调用中构建和填充。

所以现在我创建了许多集合,只是为了让它们在我返回它们的内容后立即被丢弃。我不仅为构建集合支付了少量的初始化成本,而且还为将一个集合的所有内容移动到更大的集合中付出了更大的成本。在大树中,我的大部分时间都花在将内存中的节点从集合 A 移动到 B 上。我认为这甚至使我的算法 O(n^2) 而不是 O(n),因为复制节点所花费的时间;不过如果我开始计算的话,结果可能是 O(N log(n))。

我可以使用一个简单的 getDescendants 方法来调用如下所示的辅助方法:

public Set<Node> getDescendants(){
    Set<node> descendants=new HashSet<Node>();
    getDescendantsHelper(descendants);   

    return descendants;
}

public Set<Node> getDescendantsHelper(Set<Node> descendants){

   descendants.addall(getChildren());

   for(Node child: getChildren()){
      child.getDescendantsHelper(descendant);
   }

   return nodes;
}

这样可以确保我只创建一个集合,而不必浪费时间从一个集合复制到另一个集合。但是,它需要编写两个方法而不是一个,并且通常感觉比较麻烦。

问题是,如果我担心优化这种方法,是否需要执行选项二?或者我是否可以合理地期望 java 编译器或 JIT 认识到我只是为了方便返回调用方法而创建临时集合并避免集合之间的浪费复制?

编辑:清理了导致我的示例方法将所有内容添加两次的不良复制粘贴作业。当您的“优化”代码比您的常规代码慢时,您就知道有些事情是不好的。

【问题讨论】:

  • 我会用第二种方法,不用担心它看起来很麻烦。根据我的经验,让编译器弄清楚如何优化第一个编译器是方式太多了。它似乎需要接近人类的推理能力。也许技术已经足够先进,可以做这样的事情,但我对此表示怀疑。
  • 我从未见过可以做到这一点的编译器,Java 编译器在字节码优化方面做得很少。自己编码。
  • 相关阅读:stackoverflow.com/questions/3616483/…(顺便说一句,Scala 可以进行尾递归——有时通过将递归方法编译成循环;它在 JVM 中运行。)

标签: java optimization recursion compiler-construction jit


【解决方案1】:

问题是,如果我担心优化这种方法,是否需要做选项二?

绝对是的。如果性能是一个问题(而且大多数时候不是!),那么你需要它。

编译器进行了很多优化,但规模却大不相同。基本上,它只适用于一种方法,并且优化了其中最常用的路径。由于内联繁重,它可以跨方法调用进行优化,但与上述不同。

它还可以优化掉不必要的分配,但仅限于非常简单的情况。也许像

int sum(int... a) {
    int result = 0;
    for (int x : a) result += x;
    return result;
}

调用sum(1, 2, 3) 意味着为可变参数分配int[3],这可以消除(如果编译器真的这样做,这是一个不同的问题)。它甚至可以发现结果是一个常数(我怀疑它确实如此)。如果结果没有被使用,它可以执行死代码消除(这种情况经常发生)。

您的示例涉及分配整个HashMap 及其所有条目,并且要复杂几个数量级。编译器不知道HashMap 是如何工作的,并且它无法找出例如m.addAll(m1) 之后集合m 包含m1 的所有成员。没办法。

这是一种算法优化,而不是低级的。这就是人类仍然需要的。

对于编译器可以做的事情(但目前无法做到),请参见例如我的这些关于associativitybounds checks 的问题。

【讨论】:

  • 为什么你觉得编译器无法识别 addAll?的确,这会相当复杂,但没有技术理由说这是不可能的。从实际的角度来看,最可能的优化将涉及多次内联递归调用,然后对间歇性哈希集进行逃逸分析。我认为只是一个持续的因素改进,但仍然
  • @Voo:发生了很多事情,即使是单个insert 也涉及Map.Entry 分配,找到合适的插槽,可能将其附加到链上,等等.编译器不像人类那样看到“大局”,所以它不能告诉自己“这只是一个临时映射,插入顺序无关紧要,所以让我们全部消除”。
猜你喜欢
  • 2016-07-21
  • 1970-01-01
  • 2017-06-19
  • 2015-12-16
  • 2015-01-11
  • 1970-01-01
  • 2012-03-29
  • 2012-04-07
  • 2015-09-20
相关资源
最近更新 更多