【问题标题】:When does the JVM consider code for bytecode optimization?JVM 什么时候考虑字节码优化的代码?
【发布时间】:2017-05-08 18:15:38
【问题描述】:

我正在尝试了解 JVM 和 HotSpot 优化器的内部结构。

我尽可能快地解决了用大量节点初始化对象树结构的问题。 现在,对于给定的每个树结构,我们生成 Java 源代码来初始化树,如下所示。最后,我们有数千个这样的类。

public class TypeATreeNodeInitializer {

    public TypeATreeNode initialize(){
        return getTypeATree();

    }

    private TypeATreeNode getTypeATree() {
        TypeATreeNode node = StaticTypeAFactory.create();

        TypeBTreeNode child1 = getTypeBTreeNode1(); 
        node.getChildren().add(child1);

        TypeBTreeNode child2 = getTypeBTreeNode2(); 
        node.getChildren().add(child2);

        //... may be many more children

        return node;
    }

    private TypeBTreeNode getTypeBTreeNode1() {
        TypeBTreeNode node = StaticTypeBFactory.create();

        TypeBTreeNode child1 = getTypeCTreeNode1(); 
        node.getChildren().add(child1);

        //store of value in variable first
        String value1 = "Some value";
        // assign value to node
        node.setSomeValue(value1);

        boolean value2 = false;
        node.setSomeBooleanValue(value2);


        return node;
    }



    private TypeBTreeNode getTypeCTreeNode1() {
        // ...
        return null;
    }

    private TypeBTreeNode getTypeBTreeNode2() {
        // ...
        return null;
    }

    //...  many more child node getter / initializer
}

如您所见,要分配给树节点的值首先存储在局部变量中。查看生成的字节码,结果如下:

  1. 将变量从常量池加载到堆栈 // 例如字符串“一些值”

  2. 变量在局部变量中的存储

  3. 从方法目标加载到堆栈上 // 例如TypeBTreeNode

  4. 从局部变量中加载变量 // “Some Value”

  5. setter 的调用

然而,通过不存储到局部变量并直接传递参数,这可以写得更短。所以,它变成了:

  1. 将方法目标推入堆栈 // 例如 TypeBTreeNode

  2. 然后将常量加载到堆栈中//“某些值”

  3. 然后调用setter

我知道在其他语言(例如 C++)中,编译能够进行这种优化。

在 Java 中,HotSpot 优化器在运行时负责这种魔法。
但是,据我了解文档,HotSpot 仅在第 500 次方法调用(客户端 VM)之后才启动。

问题:

  • 我是否理解正确:如果我只初始化每棵树一次,但对大量(比如说 10.000)生成的 TreeInitializer 执行此操作,则每个 TreeInitializer 都会执行第一个字节码序列,因为它们是不同的类使用不同的方法,每个方法只调用一次?

  • 我怀疑不使用本地变量重写类型器会显着加快速度,因为我节省了大约三分之一的字节码指令和可能昂贵的变量负载。我知道如果不测量就很难判断,但是更改生成器代码并非易事,所以您认为值得一试吗?

【问题讨论】:

  • 这些方法看起来很简单。为什么不尝试创建一个通用的解决方案?这可能还会节省大量加载类的时间。
  • 这个类只是示范性的。在实际应用中,非专业用户使用 DSL 编写应用逻辑。这样,他们可以创建全新的节点类型,为新字段提供不同的设置器。我可能会错过一些东西,但我认为这无法通过通用解决方案实现。
  • 嗯,从远处很难说。您不能将用户类型包装在节点中,而不是用户直接定义节点类型吗?
  • 所以你是说,用户要定义数千种不同的节点类型?听起来不是很有说服力。除此之外,如果初始化只发生一次,那么你肯定看错了。用户在初始化之后会对这些节点做一些事情,不是吗?而你对“不使用本地人显着加快重写生成器的速度”的怀疑,no,这不会发生......

标签: java performance bytecode jvm-hotspot


【解决方案1】:

像这样删除临时/堆栈变量几乎总是过早的优化。您的处理器每秒可以处理数亿条此类指令;同时,如果您正在初始化数以万计的任何东西,您的程序可能会在某个时刻阻塞等待内存分配。

我的建议始终是在您分析完代码之前推迟优化。同时,编写代码尽可能地易于阅读,这样当您确实需要回来修改某些内容时,很容易找到需要更新的地方。

【讨论】:

    【解决方案2】:

    在优化之前,JVM 会逐字节运行您的代码并分析其行为。基于此观察,它将您的代码编译为机器代码。因此,很难对此给出一般性建议。但是,您应该只将字节码视为一般抽象,而不是性能基础。

    一些经验法则:

    1. 避免使用大型方法,因为这些方法通常不会内联到其他方法中,即使 JVM 认为这是一个好主意。这是为了避免内存开销,因为内联大型方法会产生大量重复代码。
    2. 尽可能避免多态性和不稳定的分支。如果你的虚拟机发现一个方法调用只命中一个特定的类,这是个好消息。 JVM 很可能会删除此调用的虚拟属性。同样,稳定的分支可以帮助您进行分支预测。
    3. 避免为长寿命对象分配对象。如果你创建了很多对象,宁愿让它们早早死去,然后让它们长期存在。

    【讨论】:

      【解决方案3】:

      Optimize Club 的第一条规则是“不要优化”。那就是……

      将值分配给本地(堆栈)变量只是为了引用它一次已经没有意义了。如果我正在查看此代码,我会让作者删除分配并将get...() 的结果传递给add()

      这不是“过早的优化”,而是代码简化(代码质量)问题。它消除了一些字节码的事实通常也不是考虑因素,因为 JIT 编译器将在运行时优化代码。在这种情况下,因为这些初始化程序听起来它们只会运行一次,所以这种优化的阈值可能永远不会达到,因此消除不必要的堆栈分配和加载将是有价值的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-10-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多