Java编译器可以优化在递归方法中添加到集合

时间:2014-04-17 23:31:24

标签: java optimization recursion compiler-construction jit

简单的问题主要是出于对java编译器足够聪明的好奇心。我知道不是所有的编译器都是平等构建的,但是我想知道其他人是否认为在大多数编译器上进行优化是合理的,我可能会遇到这种情况,而不是它适用于特定版本或所有版本。

所以我想说我有一些树结构,我想收集一个节点的所有后代。有两种简单的方法可以递归地执行此操作。

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

public Set<Node> getDescendants(){

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

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

   return descendants;
}

然而,假设没有编译器优化和体面的树,这可能会相当昂贵。在每次递归调用中,我创建并完全填充一个集合,只返回设置堆栈,以便调用方法可以将我返回集合的内容添加到它的版本的后代集中,丢弃版本这只是在递归调用中构建和填充的。

所以现在我创建了很多套,只是为了让它们在我返回内容时被丢弃。我不仅要为构建集合支付较小的初始化成本,而且还要支付将一组中的所有内容移动到更大集合中的更大的成本。在大树中,我的大部分时间花费在内存中将节点从集合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认识到我只是为了方便返回调用方法而创建临时集并避免集合之间的浪费复制吗?

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

1 个答案:

答案 0 :(得分:1)

  

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

肯定是的。如果表现是一个问题(大部分时间都不是!),那么你需要它。

编译器优化了很多,但规模却大不相同。基本上,它只使用一种方法,它优化了最常用的路径。由于内联繁重,它可以在方法调用中进行优化,但不像上面那样。

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

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

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

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

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

对于编译器可以执行的操作(但目前无法执行),请参阅例如我的这些问题涉及associativitybounds checks