jvm 方法内联_方法内联在JVM中有多积极?

jvm 方法内联

IntelliJ IDEA中使用Ctrl + Alt + M 提取方法Ctrl + Alt + M。 这就像选择一段代码并按此组合一样简单。 Eclipse也有它 。 我讨厌冗长的方法。 对于我来说,闻起来太久了:

public void processOnEndOfDay(Contract c) {
    if (DateUtils.addDays(c.getCreated(), 7).before(new Date())) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}

首先,它具有不可读的条件。 无关紧要的实现方式,重要的是做什么。 因此,让我们首先提取它:

public void processOnEndOfDay(Contract c) {
    if (isOutdated(c)) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}
 
private boolean isOutdated(Contract c) {
    return DateUtils.addDays(c.getCreated(), 7).before(new Date());
}

显然,此方法实际上并不属于这里( F6 –移动实例方法):

public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        priorityHandling(c, OUTDATED_FEE);
        notifyOutdated(c);
        log.info("Outdated: {}", c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}

注意到不同了吗? 我的IDE使isOutdated()Contract的实例方法,听起来isOutdated() 。 但是我还是不开心。 这种方法发生了太多的事情。 一个分支执行一些与业务相关的priorityHandling() ,一些系统通知和日志记录。 其他分支执行条件通知和日志记录。 首先,让我们将处理过时的合同转移到单独的方法中:

public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        handleOutdated(c);
    } else {
        if(sendNotifications) {
            notifyPending(c);
        }
        log.debug("Pending {}", c);
    }
}
 
private void handleOutdated(Contract c) {
    priorityHandling(c, OUTDATED_FEE);
    notifyOutdated(c);
    log.info("Outdated: {}", c);
}

也许有人说这足够了,但是我看到分支之间惊人的不对称性。 handleOutdated()是非常高级的,而发送else分支是技术性的。 软件应易于阅读,因此请勿将不同级别的抽象彼此混用。 现在我很高兴:

public void processOnEndOfDay(Contract c) {
    if (c.isOutdated()) {
        handleOutdated(c);
    } else {
        stillPending(c);
    }
}
 
private void handleOutdated(Contract c) {
    priorityHandling(c, OUTDATED_FEE);
    notifyOutdated(c);
    log.info("Outdated: {}", c);
}
 
private void stillPending(Contract c) {
    if(sendNotifications) {
        notifyPending(c);
    }
    log.debug("Pending {}", c);
}

这个例子有些人为,但实际上我想证明一些不同的东西。 这些天并不经常出现,但是仍然有开发人员担心提取方法会认为它在运行时速度较慢。 他们无法理解JVM是一款很棒的软件(它可能远远超过Java语言),它内置了许多真正令人惊叹的运行时优化。 首先,较短的方法更容易推理。 流动更明显,范围更短,副作用更明显。 使用长方法,JVM可能会简单地放弃。 第二个原因更为重要:

方法内联

如果JVM发现一遍又一遍执行的小方法,它将简单地用该方法的主体替换该方法的每次调用。 以此为例:

private int add4(int x1, int x2, int x3, int x4) {
    return add2(x1, x2) + add2(x3, x4);
}
 
private int add2(int x1, int x2) {
    return x1 + x2;
}

您可能几乎可以确定,一段时间后JVM将摆脱add2()并将代码转换为:

private int add4(int x1, int x2, int x3, int x4) {
    return x1 + x2 + x3 + x4;
}

重要说明是它是JVM,而不是编译器。 生成字节码时, javac非常保守,并将所有工作都留给了JVM。 这个设计决定非常出色:

  • JVM对目标环境,CPU,内存,体系结构了解更多,并且可以更积极地进行优化
  • JVM可以发现代码的运行时特征,例如,哪些方法最常执行,哪些虚拟方法只有一种实现等。
  • 使用旧Java编译的.class将在较新的JVM上运行得更快。 您很可能会更新Java,然后重新编译源代码。

让我们对所有这些假设进行测试。 我写了一个小程序,标题为“ 有史以来最糟糕的分而治之原则add128()接受128个参数(!),并调用add64()两次–参数的前一半和后一半。 add64()类似于,只是它两次调用add32() 。 我想您会明白的,最后我们要使用add2()来完成繁重的工作。 一些数字被截断以免引起您的注意

public class ConcreteAdder {
 
    public int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
        return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
                add64(x65, x66, x67, x68, ... more ..., x127, x128);
    }
 
    private int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
        return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
                add32(x33, x34, x35, x36, ... more ..., x63, x64);
    }
 
    private int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32) {
        return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
                add16(x17, x18, x19, x20, ... more ..., x31, x32);
    }
 
    private int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16) {
        return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
    }
 
    private int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
        return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
    }
 
    private int add4(int x1, int x2, int x3, int x4) {
        return add2(x1, x2) + add2(x3, x4);
    }
 
    private int add2(int x1, int x2) {
        return x1 + x2;
    }
 
}

不难看出,通过调用add128()我们总共进行了127个方法调用。 很多。 仅供参考,这里是一个简单的实现

public class InlineAdder {
 
    public int add128n(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128) {
        return x1 + x2 + x3 + x4 + ... more ... + x127 + x128;
    }

最后,我还提供了一个使用abstract方法和继承的实现。 127个虚拟呼叫非常昂贵。 这些方法需要动态调度 ,因此要求更高,因为它们无法内联。 不能吗

public abstract class Adder {
 
    public abstract int add128(int x1, int x2, int x3, int x4, ... more ..., int x127, int x128);
 
    public abstract int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64);
 
    public abstract int add32(int x1, int x2, int x3, int x4, ... more ..., int x31, int x32);
 
    public abstract int add16(int x1, int x2, int x3, int x4, ... more ..., int x15, int x16);
 
    public abstract int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8);
 
    public abstract int add4(int x1, int x2, int x3, int x4);
 
    public abstract int add2(int x1, int x2);
}

和一个实现:

public class VirtualAdder extends Adder {
 
    @Override
    public int add128(int x1, int x2, int x3, int x4, ... more ..., int x128) {
        return add64(x1, x2, x3, x4, ... more ..., x63, x64) +
                add64(x65, x66, x67, x68, ... more ..., x127, x128);
    }
 
    @Override
    public int add64(int x1, int x2, int x3, int x4, ... more ..., int x63, int x64) {
        return add32(x1, x2, x3, x4, ... more ..., x31, x32) +
                add32(x33, x34, x35, x36, ... more ..., x63, x64);
    }
 
    @Override
    public int add32(int x1, int x2, int x3, int x4, ... more ..., int x32) {
        return add16(x1, x2, x3, x4, ... more ..., x15, x16) +
                add16(x17, x18, x19, x20, ... more ..., x31, x32);
    }
 
    @Override
    public int add16(int x1, int x2, int x3, int x4, ... more ..., int x16) {
        return add8(x1, x2, x3, x4, x5, x6, x7, x8) + add8(x9, x10, x11, x12, x13, x14, x15, x16);
    }
 
    @Override
    public int add8(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8) {
        return add4(x1, x2, x3, x4) + add4(x5, x6, x7, x8);
    }
 
    @Override
    public int add4(int x1, int x2, int x3, int x4) {
        return add2(x1, x2) + add2(x3, x4);
    }
 
    @Override
    public int add2(int x1, int x2) {
        return x1 + x2;
    }
 
}

有关@Cacheable开销的文章发表后,受到一些有趣的读者意见的鼓励,我编写了一个快速基准测试,以比较过度提取的ConcreteAdderVirtualAdder开销(以查看虚拟调用开销)。 结果出乎意料,有些模棱两可。 我在两台机器(蓝色和红色)和相同的软件上运行相同的基准测试,但是第二台具有更多内核,并且是64位:
图表

详细环境:

环境环境

事实证明,在一台速度较慢的计算机上 JVM决定内联所有内容。 不仅简单的private通话,而且虚拟的一次通话。 那怎么可能 很好,JVM发现Adder只有一个子类,因此每个abstract方法只有一个可能的版本。 如果在运行时加载另一个子类(或什至更多子类),则可能会看到性能下降,因为不再可能进行内联。 但是抛开细节,在此基准测试方法中调用并不便宜,实际上是免费的 ! 方法调用(具有极大的文档价值,提高了可读性)仅存在于源代码和字节码中。 在运行时,它们被完全消除(内联)。

我不太了解第二个基准。 看起来机器B的运行速度确实更快,但实际上运行参考SingleMethodCall基准测试的速度更快,但是其他机器甚至比A都慢。 也许它决定推迟内联? 差异是显着的,但并不是那么大。 同样,就像优化堆栈跟踪生成一样,如果您开始通过手动内联方法来优化代码,从而使它们变得更长和复杂,那么您正在解决错误的问题。

该基准可在GitHub上获得 ,以及文章来源 。 我鼓励您在设置中运行它。 此外,每个拉取请求都是自动在Travis上构建的,因此您可以在同一环境下轻松比较结果。

参考: JVM中方法内联的攻击性如何? 来自我们的JCG合作伙伴 Tomasz Nurkiewicz,来自Java和邻里博客。

翻译自: https://www.javacodegeeks.com/2013/02/how-aggressive-is-method-inlining-in-jvm.html

jvm 方法内联

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值