一尘不染

Java 8 中的方法引用缓存是个好主意吗?

python

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

   void doSomethingWithAFunc(Function<X,Y> f){...}

   void hotFunction(){
        doSomethingWithAFunc(this::func);
   }

}

假设它hotFunction经常被调用。那么是否建议缓存this::func,可能是这样的:

class Foo {
     Function<X,Y> f = this::func;
     ...
     void hotFunction(){
        doSomethingWithAFunc(f);
     }
}

就我对 java 方法引用的理解而言,当使用方法引用时,虚拟机会创建一个匿名类的对象。因此,缓存引用只会创建一次该对象,而第一种方法会在每个函数调用上创建它。这个对吗?

出现在代码中热点位置的方法引用是否应该被缓存,或者虚拟机是否能够对此进行优化并使缓存变得多余?是否有关于此的一般最佳实践,或者这种缓存是否有用,这种高度 VM 实现是否特定?


阅读 106

收藏
2022-06-27

共1个答案

一尘不染

对于无状态 lambda 或有状态 lambda,您必须区分同一调用站点的频繁执行,以及对同一方法的方法引用的频繁使用(通过不同的调用站点)。

请看以下示例:

    Runnable r1=null;
    for(int i=0; i<2; i++) {
        Runnable r2=System::gc;
        if(r1==null) r1=r2;
        else System.out.println(r1==r2? "shared": "unshared");
    }

在这里,同一个调用点被执行了两次,产生了一个无状态的 lambda,当前的实现将 print "shared"

Runnable r1=null;
for(int i=0; i<2; i++) {
  Runnable r2=Runtime.getRuntime()::gc;
  if(r1==null) r1=r2;
  else {
    System.out.println(r1==r2? "shared": "unshared");
    System.out.println(
        r1.getClass()==r2.getClass()? "shared class": "unshared class");
  }
}

在第二个示例中,同一个调用站点被执行了两次,生成一个包含对Runtime实例的引用的 lambda,当前实现将打印"unshared"but "shared class"

Runnable r1=System::gc, r2=System::gc;
System.out.println(r1==r2? "shared": "unshared");
System.out.println(
    r1.getClass()==r2.getClass()? "shared class": "unshared class");

相比之下,在最后一个示例中,两个不同的调用站点产生了一个等效的方法引用,但1.8.0_05它会打印"unshared""unshared class".


对于每个 lambda 表达式或方法引用,编译器将发出一条invokedynamic指令,该指令引用类中 JRE 提供的引导方法LambdaMetafactory和生成所需 lambda 实现类所需的静态参数。元工厂生成的内容留给实际的 JRE,但它是invokedynamic指令的指定行为,以记住和重用CallSite在第一次调用时创建的实例。

当前的 JRE 为无状态 lambda 生成ConstantCallSite包含 aMethodHandle的常量对象(并且没有可以想象的理由以不同的方式进行操作)。并且对方法的方法引用static始终是无状态的。因此,对于无状态 lambda 和单个调用站点,答案必须是:不要缓存,JVM 会做,如果不做,它必须有充分的理由不应该抵消。

对于具有参数this::func的 lambda,并且是具有对this实例的引用的 lambda,情况有些不同。允许 JRE 缓存它们,但这意味着Map在实际参数值和生成的 lambda 之间维护某种形式,这可能比再次创建简单的结构化 lambda 实例成本更高。当前的 JRE 不缓存具有状态的 lambda 实例。

但这并不意味着每次都会创建 lambda 类。这只是意味着解析后的调用站点将像一个普通的对象构造一样实例化在第一次调用时生成的 lambda 类。

类似的事情适用于对不同调用站点创建的相同目标方法的方法引用。JRE 被允许在它们之间共享单个 lambda 实例,但在当前版本中不允许,很可能是因为不清楚缓存维护是否会得到回报。在这里,即使生成的类也可能不同。


因此,像您的示例中那样进行缓存可能会使您的程序执行与没有缓存不同的事情。但不一定更有效率。缓存对象并不总是比临时对象更有效。除非您真的测量了由 lambda 创建造成的性能影响,否则您不应该添加任何缓存。

我认为,只有一些特殊情况下缓存可能有用:

  • 我们正在谈论许多不同的调用站点,它们引用相同的方法
  • lambda 是在构造函数/类初始化中创建的,因为稍后在使用站点将
  • 被多个线程同时调用
  • 遭受第一次调用的较低性能
2022-06-27