在 Java 8 中方法引用缓存是个好主意吗? [英] Is method reference caching a good idea in Java 8?

查看:29
本文介绍了在 Java 8 中方法引用缓存是个好主意吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑我有如下代码:

class Foo {

   Y func(X x) {...} 

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

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

}

假设 hotFunction 被频繁调用.那么是否建议缓存 this::func,也许像这样:

Suppose that hotFunction is called very often. Would it then be advisable to cache this::func, maybe like this:

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

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

As far as my understanding of java method references goes, the Virtual Machine creates an object of an anonymous class when a method reference is used. Thus, caching the reference would create that object only once while the first approach creates it on each function call. Is this correct?

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

Should method references that appear at hot positions in the code be cached or is the VM able to optimize this and make the caching superfluous? Is there a general best practice about this or is this highly VM-implemenation specific whether such caching is of any use?

推荐答案

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

You have to make a distinction between frequent executions of the same call-site, for stateless lambda or stateful lambdas, and frequent uses of a method-reference to the same method (by different call-sites).

看看下面的例子:

    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,当前的实现将打印 "shared".

Here, the same call-site is executed two times, producing a stateless lambda and the current implementation will 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"共享类".

In this second example, the same call-site is executed two times, producing a lambda containing a reference to a Runtime instance and the current implementation will print "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">非共享类".

In contrast, in the last example are two different call-sites producing an equivalent method reference but as of 1.8.0_05 it will print "unshared" and "unshared class".

对于每个 lambda 表达式或方法引用,编译器将发出一个 invokedynamic 指令,该指令引用类 LambdaMetafactory 以及生成所需 lambda 实现类所需的静态参数.元工厂产生什么由实际的 JRE 决定,但它是 invokedynamic 指令的指定行为来记住和重用在第一次调用时创建的 CallSite 实例.

For each lambda expression or method reference the compiler will emit an invokedynamic instruction that refers to a JRE provided bootstrap method in the class LambdaMetafactory and the static arguments necessary to produce the desired lambda implementation class. It is left to the actual JRE what the meta factory produces but it is a specified behavior of the invokedynamic instruction to remember and re-use the CallSite instance created on the first invocation.

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

The current JRE produces a ConstantCallSite containing a MethodHandle to a constant object for stateless lambdas (and there’s no imaginable reason to do it differently). And method references to static method are always stateless. So for stateless lambdas and single call-sites the answer must be: don’t cache, the JVM will do and if it doesn’t, it must have strong reasons that you shouldn’t counteract.

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

For lambdas having parameters, and this::func is a lambda that has a reference to the this instance, things are a bit different. The JRE is allowed to cache them but this would imply maintaining some sort of Map between actual parameter values and the resulting lambda which could be more costly than just creating that simple structured lambda instance again. The current JRE does not cache lambda instances having a state.

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

But this does not mean that the lambda class is created every time. It just means that the resolved call-site will behave like an ordinary object construction instantiating the lambda class that has been generated on the first invocation.

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

Similar things apply to method references to the same target method created by different call-sites. The JRE is allowed to share a single lambda instance between them but in the current version it doesn’t, most probably because it is not clear whether the cache maintenance will pay off. Here, even the generated classes might differ.

所以像你的例子中的缓存可能会让你的程序做不同的事情.但不一定更有效.缓存对象并不总是比临时对象更有效.除非您真的衡量 lambda 创建造成的性能影响,否则不应添加任何缓存.

So caching like in your example might have your program do different things than without. But not necessarily more efficient. A cached object is not always more efficient than a temporary object. Unless you really measure a performance impact caused by a lambda creation, you should not add any caching.

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

I think, there are only some special cases where caching might be useful:

  • 我们正在讨论许多不同的调用站点,它们都引用了相同的方法
  • lambda 是在构造函数/类初始化中创建的,因为稍后使用站点将
    • 被多个线程同时调用
    • 遭受第一次调用的性能下降
    • we are talking about lots of different call-sites referring to the same method
    • the lambda is created in the constructor/class initialize because later on the use-site will
      • be called by multiple threads concurrently
      • suffer from the lower performance of the first invocation

      这篇关于在 Java 8 中方法引用缓存是个好主意吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆