43

我正在编写一些调用Field.setField.get成千上万次的代码。显然这是因为反射非常慢。

我想看看我是否可以提高使用MethodHandleJava 7 的性能。到目前为止,这就是我所拥有的:

而不是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 的重复调用?

4

4 回答 4

72

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 在这种特殊情况下确实比反射快得多(这是因为对私有字段的访问检查是在查找时完成的,而不是在调用时完成的)。dynamic_*案例模拟MethodHandles和/或Fields不是静态已知的情况,例如从中拉出Map<String, MethodHandle>或类似的情况。相反,static_*情况是调用者是静态已知的。

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

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

于 2014-03-11T22:09:27.497 回答
20

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

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


原答案:

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

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

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

直接:27,415ns
反射:1088,462ns
方法句柄:7133,221ns
mh 调用精确:60,928ns
通用 mh : 68,025ns

并且使用-serverJVM 会让人感到莫名其妙

直接:26,953ns
反射:629,161ns
方法句柄:1513,226ns
mh 调用精确:22,325ns
通用 mh : 43,608ns

我认为看到 aMethodHandle比直接操作更快并没有太大的现实意义,但它证明MethodHandles 在 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);
    }
  }
}
于 2014-03-11T09:37:03.720 回答
6

编辑感谢 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");
        }
    }
}

然后我在 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");
        }
    }
}

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

至于为什么你没有看到进步……很难说。我所做的是微基准测试。这根本无法说明真实的应用程序。但是使用这些结果我会假设您使用旧的 jdk 版本,或者您没有足够频繁地重用句柄。因为虽然执行句柄可以更快,但创建句柄的成本可能比创建字段的成本高得多。

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

于 2014-03-11T10:18:57.810 回答
6

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

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

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

}
于 2018-01-24T10:08:43.763 回答