Java编译器可以优化递归方法中的向集合添加吗

Java编译器可以优化递归方法中的向集合添加吗,java,optimization,recursion,compiler-construction,jit,Java,Optimization,Recursion,Compiler Construction,Jit,问这个简单的问题主要是出于对java编译器的哪些功能足够聪明的好奇。我知道并非所有编译器都是平等构建的,但我想知道其他人是否认为在我可能运行的大多数编译器上进行优化是合理的,而不是在特定版本或所有版本上进行优化 假设我有一些树结构,我想收集一个节点的所有后代。有两种简单的递归方法 对我来说,更自然的方法是: public Set<Node> getDescendants(){ Set<Node> descendants=new HashSet<Node>

问这个简单的问题主要是出于对java编译器的哪些功能足够聪明的好奇。我知道并非所有编译器都是平等构建的,但我想知道其他人是否认为在我可能运行的大多数编译器上进行优化是合理的,而不是在特定版本或所有版本上进行优化

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

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

public Set<Node> getDescendants(){

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

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

   return descendants;
}
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;
}
publicset getsubstands(){
Set后代=新HashSet();
addall(getChildren());
对于(节点子节点:getChildren()){
addall(child.getDescents());
}
传宗接代;
}
然而,假设没有编译器优化和一个相当大的树,这可能会非常昂贵。在每次递归调用中,我创建并完全填充一个集合,只返回设置堆栈的集合,以便调用方法可以将返回集合的内容添加到其后代集合的版本中,丢弃在递归调用中刚刚构建和填充的版本

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

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

public Set<Node> getDescendants(){

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

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

   return descendants;
}
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;
}
publicset getsubstands(){
Set后代=新HashSet();
GetDegenantShelper(后代);
传宗接代;
}
公共集getDescendantsHelper(集子体){
addall(getChildren());
对于(节点子节点:getChildren()){
child.getgenderantshelper(后代);
}
返回节点;
}
这确保了我只创建一个集合,而不必浪费时间从一个集合复制到另一个集合。但是,它需要编写两种方法,而不是一种方法,并且通常感觉有点麻烦

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

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

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

当然可以。如果性能是一个问题(大多数情况下并非如此!),那么您需要它

编译器进行了大量优化,但规模却大不相同。基本上,它只使用一种方法,并且优化了其中最常用的路径。由于大量的内联,它可以跨方法调用进行某种程度的优化,但与上面的情况不同

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

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
的所有成员。不可能

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


有关编译器可以做的事情(但目前未能做到),请参见我的这些关于和的问题。

我会用第二种方法来做,不必担心它看起来很麻烦。根据我的经验,让一个编译器来找出如何优化第一个编译器的要求太多了;这似乎需要近乎人类的推理能力。也许这项技术已经足够先进,可以做这样的事情,但我对此表示怀疑。我从未见过一个编译器可以做到这一点,而Java编译器在字节码优化方面做得很少。相关阅读:(顺便说一句,Scala可以进行尾部递归——有时通过将递归方法编译成循环;它在JVM中运行。)为什么您觉得编译器不能理解addAll?的确,这是一个相当复杂的问题,但没有技术上的理由来说明这是不可能的。从实用的角度来看,最有可能的优化包括多次内联递归调用,然后对间歇散列集进行转义分析。我认为这只是一个持续的因素,但still@Voo:发生了很多事情,即使是一个
insert
也会涉及到
映射。输入
分配,找到合适的插槽,可能会将其附加到链中,等等。编译器没有像人类那样看到“大图”,所以它不能告诉自己“这只是一个临时映射,插入顺序无关紧要,所以让我们把它全部消除”。