如何通过使用MethodHandles来提高Field.set的性能?

45

我正在编写一些调用 Field.setField.get 数千次的代码。显然,由于反射,这非常慢。

我想看看能否使用Java 7中的MethodHandle来提高性能。到目前为止,我的做法如下:

与其使用field.set(pojo, value),我现在使用的是:

private static final Map<Field, MethodHandle> setHandles = new HashMap<>();

MethodHandle mh = setHandles.get(field);
if (mh == null) {
    mh = lookup.unreflectSetter(field);
    setHandles.put(field, mh);
}
mh.invoke(pojo, value);

不过,这似乎不如使用反射的 Field.set 调用来表现更好。我在这里做错了什么吗?

我读到使用 invokeExact 可能更快,但当我尝试使用它时,我遇到了一个 java.lang.invoke.WrongMethodTypeException

有人成功地优化过重复调用 Field.set 或 Field.get 吗?


1
在Java 7中,MethodHandle可能会很慢。有一次我们也尝试用它们替换反射调用,结果它们实际上更糟糕了。希望在Java 8中情况会变得更好,因为MethodHandle被用于创建lambda类。建议在JDK8上进行测试。 - ghik
3
你为什么要使用 Field.get/set 和反射?你想实现什么样的目标(更高层次的问题)?请记住 Java 中的忠告:“对于类的实例,可以使用反射来设置该类中字段的值。通常只在无法以常规方式设置值时才这样做。因为这种访问通常违反了类的设计意图,所以应该极度谨慎使用。” - ErstwhileIII
1
我同意@ErstwhileIII的观点。如果你正在执行这么多次以至于性能成为瓶颈,那你已经做错了一些事情。考虑定义和实现一个接口。 - user207421
2
我正在使用Objecitfy(一种适用于appengine的ORM)。这是一个库,可以在注册的POJO和appengine数据存储实体之间进行转换。这特别好,因为您不必创建两个结构之间的转换器,它会使用反射自动转换。我想修改此库,使其更具性能,在对其进行分析后,很明显field.set/get是大部分性能瓶颈的原因。 - aloo
4个回答

74

2015-06-01: 根据@JoeC的评论更新,涉及到另一个处理静态句柄的情况。同时使用最新版本的JMH重新运行测试,适用于现代硬件。结论基本保持不变。

请进行适当的基准测试,使用JMH可能并不那么困难。一旦你这样做了,答案就变得显而易见了。它还可以展示invokeExact的正确使用(需要目标/源1.7才能编译和运行):

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class MHOpto {

    private int value = 42;

    private static final Field static_reflective;
    private static final MethodHandle static_unreflect;
    private static final MethodHandle static_mh;

    private static Field reflective;
    private static MethodHandle unreflect;
    private static MethodHandle mh;

    // We would normally use @Setup, but we need to initialize "static final" fields here...
    static {
        try {
            reflective = MHOpto.class.getDeclaredField("value");
            unreflect = MethodHandles.lookup().unreflectGetter(reflective);
            mh = MethodHandles.lookup().findGetter(MHOpto.class, "value", int.class);
            static_reflective = reflective;
            static_unreflect = unreflect;
            static_mh = mh;
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new IllegalStateException(e);
        }
    }

    @Benchmark
    public int plain() {
        return value;
    }

    @Benchmark
    public int dynamic_reflect() throws InvocationTargetException, IllegalAccessException {
        return (int) reflective.get(this);
    }

    @Benchmark
    public int dynamic_unreflect_invoke() throws Throwable {
        return (int) unreflect.invoke(this);
    }

    @Benchmark
    public int dynamic_unreflect_invokeExact() throws Throwable {
        return (int) unreflect.invokeExact(this);
    }

    @Benchmark
    public int dynamic_mh_invoke() throws Throwable {
        return (int) mh.invoke(this);
    }

    @Benchmark
    public int dynamic_mh_invokeExact() throws Throwable {
        return (int) mh.invokeExact(this);
    }

    @Benchmark
    public int static_reflect() throws InvocationTargetException, IllegalAccessException {
        return (int) static_reflective.get(this);
    }

    @Benchmark
    public int static_unreflect_invoke() throws Throwable {
        return (int) static_unreflect.invoke(this);
    }

    @Benchmark
    public int static_unreflect_invokeExact() throws Throwable {
        return (int) static_unreflect.invokeExact(this);
    }

    @Benchmark
    public int static_mh_invoke() throws Throwable {
        return (int) static_mh.invoke(this);
    }

    @Benchmark
    public int static_mh_invokeExact() throws Throwable {
        return (int) static_mh.invokeExact(this);
    }

}

在1x4x2 i7-4790K,JDK 8u40,Linux x86_64上运行,得到如下结果:

Benchmark                             Mode  Cnt  Score   Error  Units
MHOpto.dynamic_mh_invoke              avgt   25  4.393 ± 0.003  ns/op
MHOpto.dynamic_mh_invokeExact         avgt   25  4.394 ± 0.007  ns/op
MHOpto.dynamic_reflect                avgt   25  5.230 ± 0.020  ns/op
MHOpto.dynamic_unreflect_invoke       avgt   25  4.404 ± 0.023  ns/op
MHOpto.dynamic_unreflect_invokeExact  avgt   25  4.397 ± 0.014  ns/op
MHOpto.plain                          avgt   25  1.858 ± 0.002  ns/op
MHOpto.static_mh_invoke               avgt   25  1.862 ± 0.015  ns/op
MHOpto.static_mh_invokeExact          avgt   25  1.859 ± 0.002  ns/op
MHOpto.static_reflect                 avgt   25  4.274 ± 0.011  ns/op
MHOpto.static_unreflect_invoke        avgt   25  1.859 ± 0.002  ns/op
MHOpto.static_unreflect_invokeExact   avgt   25  1.858 ± 0.002  ns/op

这表明在这种特定情况下,MH确实比Reflection快得多(这是因为对私有字段的访问检查在查找时完成,而不是在调用时完成)。dynamic_*案例模拟了MethodHandles和/或Fields不是静态已知的情况,例如从Map<String, MethodHandle>中提取或类似的情况。相反,static_*案例是那些调用者是静态已知的情况。

请注意,在dynamic_*案例中,反射性能与MethodHandles相当,这是因为在JDK 8中反射被进一步优化(因为实际上,您不需要访问检查来读取自己的字段),因此答案可能是“只需”切换到JDK 8 ;)

static_*案例甚至更快,因为MethoHandles.invoke调用被积极地内联。这消除了MH案例中的部分类型检查。但是,在反射案例中仍然存在快速检查,因此它落后于MH。


4
令人难以置信,你只是说了和我一样的话就获得了大量的赞,而我的回答却受到质疑,即使你的高评分回答证明了结果。而你甚至没有回答如何解决aloo无法使用invokExact的实际问题... - Holger
7
我认为,如果你进行适当的基准测试,答案是显而易见的。 - Aleksey Shipilev
8
@Holger 这个回答因为使用了被接受的基准测试技术而被点赞。JVM 太聪明了,有时甚至可以超越 JMH 基准测试。你的基准测试技术忽略了 JVM 的复杂性,并且误解了使用纳秒时间来测量纳秒级调用的问题。 - Xorlev
2
虽然这是一个很好的基准,但你没有让JIT发挥其所擅长的:将所有内容都内联起来。当你将Handles / Methods标记为private static final时,性能会有非常不同的表现。相比反射,invokeExact最终表现最佳,快2.6倍。我还添加了invokeSpecial进行比较,链接到完整结果https://gist.github.com/mooman219/f85c6560cb550a9e3b28 - Joe C
3
@JoeC:是的,谢谢,这也是一个明智的案例。我已经将其添加到更新后的答案中。动态情况也很重要,因为很多时候人们使用“Map<String,MethodHandle/Field>”来查找getter。 - Aleksey Shipilev
显示剩余5条评论

20
更新:由于一些人开始了一个无意义的“如何进行基准测试”的讨论,我将强调我的答案中包含的解决方案,现在放在开头:
即使在反射上下文中使用invokeExact,您也可以通过使用asTypeMethodHandle转换为以Object为参数的句柄来解决问题。在受到invokeinvokeExact之间性能差异影响的环境中,使用这样一个转换处理程序上的invokeExact仍然比在直接方法句柄上使用invoke快得多。
原始答案:
问题确实是您没有使用invokeExact。以下是一个小型基准测试程序,显示了不同方法递增int字段的结果。使用invoke而不是invokeExact会导致性能下降到低于反射的速度。
您收到WrongMethodTypeException,因为MethodHandle是强类型的。它期望一个精确的调用签名,匹配字段和所有者的类型。但是,您可以使用该句柄创建一个新的MethodHandle,其中包含必要的类型转换。在具有通用签名(即(Object,Object)Object)的处理程序上使用invokeExact仍将比使用动态类型转换的invoke更有效率。
我的机器上使用1.7.0_40的结果如下:
直接操作:   27,415ns
反射操作: 1088,462ns
方法句柄: 7133,221ns
mh invokeExact:   60,928ns
通用方法句柄:   68,025ns
并且使用-server JVM会产生令人困惑的结果:
直接操作:   26,953ns
反射操作:  629,161ns
方法句柄: 1513,226ns
mh invokeExact:   22,325ns
通用方法句柄:   43,608ns
我认为这没有太多实际意义,因为看到MethodHandle比直接操作更快,但它证明了在Java7上MethodHandle不慢。
而通用MethodHandle仍然优于反射(而使用invoke则不是)。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;

public class FieldMethodHandle
{
  public static void main(String[] args)
  {
    final int warmup=1_000_000, iterations=1_000_000;
    for(int i=0; i<warmup; i++)
    {
      incDirect();
      incByReflection();
      incByDirectHandle();
      incByDirectHandleExact();
      incByGeneric();
    }
    long direct=0, refl=0, handle=0, invokeExact=0, genericH=0;
    for(int i=0; i<iterations; i++)
    {
      final long t0=System.nanoTime();
      incDirect();
      final long t1=System.nanoTime();
      incByReflection();
      final long t2=System.nanoTime();
      incByDirectHandle();
      final long t3=System.nanoTime();
      incByDirectHandleExact();
      final long t4=System.nanoTime();
      incByGeneric();
      final long t5=System.nanoTime();
      direct+=t1-t0;
      refl+=t2-t1;
      handle+=t3-t2;
      invokeExact+=t4-t3;
      genericH+=t5-t4;
    }
    final int result = VALUE.value;
    // check (use) the value to avoid over-optimizations
    if(result != (warmup+iterations)*5) throw new AssertionError();
    double r=1D/iterations;
    System.out.printf("%-14s:\t%8.3fns%n", "direct", direct*r);
    System.out.printf("%-14s:\t%8.3fns%n", "reflection", refl*r);
    System.out.printf("%-14s:\t%8.3fns%n", "method handle", handle*r);
    System.out.printf("%-14s:\t%8.3fns%n", "mh invokeExact", invokeExact*r);
    System.out.printf("%-14s:\t%8.3fns%n", "generic mh", genericH*r);
  }
  static class MyValueHolder
  {
    int value;
  }
  static final MyValueHolder VALUE=new MyValueHolder();

  static final MethodHandles.Lookup LOOKUP=MethodHandles.lookup();
  static final MethodHandle DIRECT_GET_MH, DIRECT_SET_MH;
  static final MethodHandle GENERIC_GET_MH, GENERIC_SET_MH;
  static final Field REFLECTION;
  static
  {
    try
    {
      REFLECTION = MyValueHolder.class.getDeclaredField("value");
      DIRECT_GET_MH = LOOKUP.unreflectGetter(REFLECTION);
      DIRECT_SET_MH = LOOKUP.unreflectSetter(REFLECTION);
      GENERIC_GET_MH = DIRECT_GET_MH.asType(DIRECT_GET_MH.type().generic());
      GENERIC_SET_MH = DIRECT_SET_MH.asType(DIRECT_SET_MH.type().generic());
    }
    catch(NoSuchFieldException | IllegalAccessException ex)
    {
      throw new ExceptionInInitializerError(ex);
    }
  }

  static void incDirect()
  {
    VALUE.value++;
  }
  static void incByReflection()
  {
    try
    {
      REFLECTION.setInt(VALUE, REFLECTION.getInt(VALUE)+1);
    }
    catch(IllegalAccessException ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByDirectHandle()
  {
    try
    {
      Object target=VALUE;
      Object o=GENERIC_GET_MH.invoke(target);
      o=((Integer)o)+1;
      DIRECT_SET_MH.invoke(target, o);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByDirectHandleExact()
  {
    try
    {
      DIRECT_SET_MH.invokeExact(VALUE, (int)DIRECT_GET_MH.invokeExact(VALUE)+1);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByGeneric()
  {
    try
    {
      Object target=VALUE;
      Object o=GENERIC_GET_MH.invokeExact(target);
      o=((Integer)o)+1;
      o=GENERIC_SET_MH.invokeExact(target, o);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
}

1
我有一些评论要发表。使用final static来处理对性能有积极影响,但aloo能否使用呢?我怀疑这一点。此外,你在微基准测试中犯了一个典型的错误,即同时对多个事物进行基准测试。如果这样做,测试的顺序往往会影响JVM。当然,我没有检查这是否也是这种情况。 - blackdrag
1
同时做几件事情可能会影响结果,但不一定是坏的。预期使用案例将是ORM,因此我不希望实际情况仅包含MethodHandle的使用。鉴于结果的明显数量,字段的static final属性并不太重要。如果结果小于十的因素,则我不会从中得出任何结论,但三十或更多的因素允许得出结论。 - Holger
2
然后我刚刚仔细检查了一下...试一试吧,当你修改了基准以实际计算发生的时间时,将incByDirectHandleExact更改为在执行之前将句柄存储在局部变量中。在我的情况下,我将外部循环移到了方法内部,并且当然的赋值是在那个循环之外的。结果是使用最终的静态字段要花费15倍的时间。这是由于一种特殊的优化,在只有使用最终的静态字段时才能进行。我曾经认为它可能在后来的jdk7中被删除了,但似乎不是这样的情况。 - blackdrag
4
我的结果并不能证明你的结果是正确的:你使用了不完整的技术来获得结果。仅仅因为它产生了相似的结果,并不意味着我的结果能够验证你获得结果的方式。 - Aleksey Shipilev
2
@scottxiao 没有每次调用的安全检查,没有基本类型的自动装箱,没有为参数创建数组,根据情况,JVM 优化更好。但请注意,这并不是保证。这个问答的主题甚至不是反射 vs mh,而是 mh 未能提供高性能(仅影响旧版 JVM)。对于你的问题,这个答案 可能更适合。它表明,根据情况,反射可以像 mh 一样快。 - Holger
显示剩余4条评论

6

在JDK 7和8中,MethodHandles存在一个“catch 22”(我还没有测试过JDK 9或更高版本):如果它在静态字段中,则MethodHandle很快(与直接访问一样快)。否则,它们就像反射一样慢。如果您的框架需要在n个getter或setter上进行反射,其中n在编译时未知,则MethodHandles可能对您无用。

我写了一篇文章,对加速反射的所有不同方法进行了基准测试

使用LambdaMetafactory(或更多外来方法,如代码生成)加速调用getter和setter。这是一个获取器的要点(对于设置器,请使用BiConsumer):

public final class MyAccessor {

    private final Function getterFunction;

    public MyAccessor() {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(lookup,
                "apply",
                MethodType.methodType(Function.class),
                MethodType.methodType(Object.class, Object.class),
                lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class)),
                MethodType.methodType(String.class, Person.class));
        getterFunction = (Function) site.getTarget().invokeExact();
    }

    public Object executeGetter(Object bean) {
        return getterFunction.apply(bean);
    }

}

6

编辑 感谢holger提醒我,我注意到我确实应该使用invokeExact,因此我决定删除有关其他jdk的内容,只使用invokeExact... 对于我来说,使用-server还是不使用并没有真正的区别

使用反射和使用MethodHandles的主要区别在于,对于每个调用,反射需要进行安全检查,而MethodHandles仅在创建句柄时进行检查。

如果你看一下这个:

class Test {
    public Object someField;
    public static void main(String[] args) throws Exception {
        Test t = new Test();
        Field field = Test.class.getDeclaredField("someField");
        Object value = new Object();
        for (int outer=0; outer<50; outer++) {
            long start = System.nanoTime();
            for (int i=0; i<100000000; i++) {
                field.set(t, value);
            }
            long time = (System.nanoTime()-start)/1000000;
            System.out.println("it took "+time+"ms");
        }
    }
}

然后我在我的电脑上运行了45000ms的jdk7u40(虽然jdk8和pre 7u25表现更好)

现在让我们看一下使用句柄的相同程序

class Test {
    public Object someField;
    public static void main(String[] args) throws Throwable {
        Test t = new Test();
        Field field = Test.class.getDeclaredField("someField");
        MethodHandle mh = MethodHandles.lookup().unreflectSetter(field);
        Object value = new Object();
        for (int outer=0; outer<50; outer++) {
            long start = System.nanoTime();
            for (int i=0; i<100000000; i++) {
                mh.invokeExact(t, value);
            }
            long time = (System.nanoTime()-start)/1000000;
            System.out.println("it took "+time+"ms");
        }
    }
}

7u40表示大约需要1288毫秒。因此,我可以确认Holger在7u40上的30次。在7u06上,这段代码会更慢,因为反射速度快了几倍,在jdk8上则完全不同。

至于为什么您没有看到改进...很难说。我所做的是微基准测试。这根本无法说明真正应用程序的情况。但是使用这些结果,我会认为您要么使用旧的jdk版本,要么没有经常重复使用句柄。因为虽然执行句柄可能更快,但创建句柄的成本可能比创建字段高得多。

现在最大的问题点...我确实看到您想要这个应用于Google App Engine...我必须说,您可以在本地进行尽可能多的测试,但最终的应用程序性能才是最重要的。据我所知,他们使用了修改过的OpenJDK,但使用何种版本以及何种修改他们并没有透露。由于Jdk7非常不稳定,您可能会运气不佳或者不会。也许他们为反射添加了特殊代码,那么所有的赌注都是错的。即使忽略这一点...也许付款模型再次发生了变化,但通常您希望通过缓存来避免数据存储访问,因为它的成本很高。如果这仍然成立,那么任何句柄平均会被调用多少次是现实的呢?


1
对于当前的64位JVM,没有客户端JVM,因此显然-server对于64位没有任何影响。据我所知,分层编译应该最终取代客户端/服务器JVM模型。因此,很可能永远不会有64位客户端JVM。 - Holger
是的,我知道。而且分层编译经常会带来更多麻烦,而不是解决问题。我希望这种情况在未来会有所改变,但目前我通常将其关闭。 - blackdrag

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接