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

时间:2014-03-07 07:47:56

标签: java performance reflection field methodhandle

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

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

而不是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 个答案:

答案 0 :(得分:59)

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_*个案例是那些静态知道调用者的案例。

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

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

答案 1 :(得分:13)

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

您可以使用invokeExact,即使在您没有确切类型签名的反思背景下,也可以将MethodHandle使用asType转换为Object的句柄参数。在受invokeinvokeExact之间性能差异影响的环境中,在此类转换句柄上使用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

并使用-server JVM产生令人困惑的

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

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

通用MethodHandle仍然会超越反射(虽然使用MethodHandle不会)。

invoke

答案 2 :(得分:3)

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

我写了an article that benchmarked all the different approaches to speed up reflection

使用LambdaMetafactory(或更奇特的方法,如代码生成)来加速调用getter和setter。以下是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);
    }

}

答案 3 :(得分:2)

编辑感谢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(jdk8和pre 7u25上表现得好得多)上我的计算机时间45000ms

现在让我们使用句柄

查看相同的程序
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版本,或者您不经常重用该句柄。因为虽然执行句柄可以更快,但是创建句柄可能比创建字段花费更多。

现在最大的问题点......我确实看到你想要这个用于google appengine ......而且我必须说,你可以根据自己的需要进行本地测试,最重要的是应用程序的性能是什么在谷歌网站上将是。 Afaik他们使用修改过的OpenJDK,但他们没有说什么修改版本。由于Jdk7不稳定,你可能不幸或不幸。也许他们添加了特殊的反射代码,然后所有的赌注都是关闭的。甚至忽略了......也许付款模式再次改变,但通常你想通过缓存来避免数据存储访问,因为它的成本。如果仍然存在,那么现在是否可以调用任何句柄让我们平均说出10.000倍?