如何提高Field.set的性能(使用MethodHandles进行perhap)?

alo*_*loo 38 java reflection performance field methodhandle

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

我想看看我是否可以MethodHandle在Java7中提高性能.到目前为止,这就是我所拥有的:

而不是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);
Run Code Online (Sandbox Code Playgroud)

但是,这似乎没有比使用反射的Field.set调用更好.我在这里做错了吗?

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

有没有人成功地优化了对Field.set或Field.get的重复调用?

Ale*_*lev 62

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

...这表明MH在这种特殊情况下实际上比Reflection快得多(这是因为对私有字段的访问检查是在查找时完成的,而不是在调用时完成的).dynamic_*案例模拟了MethodHandles和/或Fields不是静态知道的情况,例如拉出Map<String, MethodHandle>或类似的情况.相反,static_*案例是那些静态知道的工具.

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

static_*案件甚至更快,因为MethoHandles.invoke呼叫是积极内联的.这消除了MH情况下的部分类型检查.但是,在反思案例中,仍然存在快速检查,因此,它落后了.

  • @Holger这个答案被投了票,因为它使用了公认的基准测试技术.JVM太聪明了,有时甚至超过了JMH基准.您的基准测试技术显示无视JVM复杂性,以及在使用纳米级测量纳秒级调用时对问题的误解. (8认同)
  • 我提出,当您进行适当的基准测试时,答案是显而易见的. (7认同)
  • @JoeC:是的,谢谢,这也是一个明智的案例.我把它添加到更新的答案中.动态情况也很重要,因为人们经常使用`Map <String,MethodHandle/Field>`来查找getter. (3认同)
  • 令人难以置信的是,即使你的评价很高的答案证明了结果,你也会得到大量的赞成,只是说我的答案比我的要好.你甚至没有回答如何*解决*aloo的实际问题,即无法使用`invokExact` ... (2认同)
  • OP的原始问题是关于优化Field.get()性能,可能使用MethodHandles,我的回答就是这样.但是你对`invokeExact`的评论有点公平,我隐含地想出现使用`invokeExact`的工作代码来回答如何使用它的问题. (2认同)
  • 虽然它是一个很好的基准,但你不会让JIT做它最擅长的事情:将一切都归咎于内心.当您将Handles/Methods标记为私有静态final时,您会得到一个非常不同的性能图片.invokeExact最终名列前茅,比反射快2.6倍.我还添加了invokeSpecial进行比较,链接到完整的结果https://gist.github.com/mooman219/f85c6560cb550a9e3b28 (2认同)

Hol*_*ger 16

更新:由于有些人开始就"如何进行基准测试"进行毫无意义的讨论,我会强调我的答案中包含的问题的解决方案,现在就在开头:

invokeExact通过将MethodHandle使用转换为作为参数asType的句柄,您甚至可以在您没有确切类型签名的反射上下文中使用Object.在受到invoke和之间性能差异影响的环境中invokeExact,invokeExact在这样的转换句柄上使用仍然比invoke在直接方法句柄上使用更快.


原始答案:

问题确实是你没有使用invokeExact.下面是一个小的基准程序,显示了增加int字段的不同方法的结果.使用invoke而不是invokeExact导致性能下降到反射速度以下.

你收到了WrongMethodTypeException因为MethodHandle强类型.它期望精确的调用签名匹配字段和所有者的类型类型.但是您可以使用句柄创建一个新的MethodHandle包装必要的类型转换.使用invokeExact通用签名(即(Object,Object)Object)在该句柄上使用将比使用invoke动态类型转换更有效.

我的机器使用1.7.0_40的结果是:

direct        :   27,415ns
reflection    : 1088,462ns
method handle : 7133,221ns
mh invokeExact:   60,928ns
generic mh    :   68,025ns

并使用-serverJVM产生令人困惑的结果

direct        :   26,953ns
reflection    :  629,161ns
method handle : 1513,226ns
mh invokeExact:   22,325ns
generic mh    :   43,608ns

我不认为它看起来MethodHandle比直接操作更快,但它证明MethodHandle在Java7上的速度并不慢.

而泛型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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 好吧,我的结果不能证明你的结果:你的结果是用破碎的技术获得的.仅仅产生类似结果的事实并不意味着我的结果以某种方式验证了获得结果的方式. (4认同)
  • 然后我只是仔细检查了......尝试一下,在你修改你的基准测试之后,实际需要更改incByDirectHandleExact以在执行前将句柄存储在局部变量中.在我的情况下,我移动了方法内部的外部循环,当然从该循环中分配.结果是它需要15倍的时间,然后使用最终的静态字段.这是因为只有在使用最终静态字段时才能进行特殊优化.我认为它可能在以后的jdk7中被删除了,但似乎并非如此 (2认同)
  • @scottxiao 没有每次调用的安全检查,没有原始值的自动装箱,没有为参数创建数组,根据情况,更好的 JVM 优化。但请注意,这并不能得到保证。本次问答的主题甚至不是反射与 mh 的对比,而是关于 mh 无法提供高性能(仅影响较旧的 JVM)。对于您的问题,[这个答案](/sf/answers/1369410031/)可能更适合。它表明,根据具体情况,反射可以与 mh 一样快。 (2认同)

bla*_*rag 5

编辑感谢holger我注意到我真的应该使用invokeExact,所以我决定删除关于其他jdks的东西并只使用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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我在 jdk7u40 上使用我的计算机时间为 45000 毫秒(尽管 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");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

7u40 表示大约 1288 毫秒。所以我可以在 7u40 上确认 Holger 的 30 次。在 7u06 上,此代码处理会更慢,因为反射要快几倍,而在 jdk8 上,一切又都是新的。

至于为什么你没有看到改善......很难说。我所做的是微基准测试。这根本无法说明实际应用程序的任何情况。但是使用这些结果,我会假设您要么使用旧的 jdk 版本,要么不经常重复使用句柄。因为虽然执行句柄可以更快,但句柄的创建可能比字段的创建花费更多。

现在最大的问题点......我确实看到你想要谷歌应用引擎......而且我必须说,你可以根据需要在本地进行测试,最终重要的是应用程序在谷歌上的性能网站将。Afaik 他们使用修改后的 OpenJDK,但是他们没有说什么版本有什么修改。由于 Jdk7 如此不稳定,您可能不走运。也许他们为反射添加了特殊的代码,然后所有的赌注都被取消了。甚至忽略这一点......也许支付模式再次发生变化,但通常您希望通过缓存来避免数据存储访问,因为它会产生成本。如果这仍然成立,那么任何句柄将被称为平均 10.000 次是否现实?


Geo*_*met 5

在JDK 7和8中,MethodHandles 有一个问题22(我尚未测试JDK 9或更高版本):如果MethodHandle在静态字段中,则它速度很快(与直接访问一样快)。否则,它们会像反射一样缓慢。如果您的框架反映了n个getter或setter,在编译时不知道n,则MethodHandles对您可能没有用。

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

使用LambdaMetafactory(或其他更奇怪的方法,例如代码生成)来加快调用getter和setter的速度。这是获取方法的要点(对于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);
    }

}
Run Code Online (Sandbox Code Playgroud)