web-dev-qa-db-ja.com

Field.setのパフォーマンスを改善するにはどうすればよいですか(おそらくMethodHandlesを使用して)?

_Field.set_ と_Field.get_を何千回も呼び出すコードを書いています。 反射 のため、明らかにこれは非常に遅いです。

MethodHandle in Java 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への繰り返しの呼び出しを最適化できた人はいますか?

38
aloo

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でさらに大幅に最適化されているためです(実際には、独自のフィールドを読み取るためにアクセスチェックが必要ないため)。 JDK8に「ただ」切り替えるだけかもしれません;)

static_*呼び出しは積極的にインライン化されるため、MethoHandles.invokeケースはさらに高速になります。これにより、MHの場合の型チェックの一部が不要になります。ただし、リフレクションの場合は、まだクイックチェックが存在するため、遅れています。

64

更新:「ベンチマークの方法」について無意味な議論を始めた人もいるので、私の答えに含まれている問題の解決策を強調します。始まり:

invokeExact を使用してMethodHandleをハンドルに変換することにより、正確な型シグネチャがないリフレクティブコンテキストでもasTypeを使用できます。 Objectを引数として取ります。 invokeinvokeExactのパフォーマンスの違いの影響を受ける環境では、このような変換ハンドルでinvokeExactを使用する方が、直接メソッドでinvokeを使用するよりもはるかに高速です。扱う。


元の答え:

問題は、実際にinvokeExactを使用していないことです。以下は、intフィールドをインクリメントするさまざまな方法の結果を示す小さなベンチマークプログラムです。 invokeの代わりにinvokeExactを使用すると、パフォーマンスがReflectionの速度を下回ります。

WrongMethodTypeExceptionは強く型付けされているため、MethodHandleを受け取ります。フィールドと所有者のタイプタイプに一致する正確な呼び出しシグネチャが必要です。ただし、ハンドルを使用して、必要な型変換をラップする新しいMethodHandleを作成できます。ジェネリック署名(つまり、(Object,Object)Object)を使用してそのハンドルでinvokeExactを使用すると、動的型変換でinvokeを使用するよりもはるかに効率的です。

1.7.0_40を使用した私のマシンでの結果は次のとおりです。

直接:27,415ns 
リフレクション:1088,462ns 
メソッドハンドル:7133,221ns 
 mh invokeExact:60,928ns 
 generic mh:68,025ns 

-server JVMを使用すると、困惑することになります

直接:26,953ns 
リフレクション:629,161ns 
メソッドハンドル:1513,226ns 
 mh invokeExact:22,325ns 
 generic mh:43,608ns 

MethodHandleが直接操作よりも高速であるため、実際の関連性はあまりないと思いますが、Java7ではMethodHandlesが遅くないことを証明しています。

また、汎用のMethodHandleは依然としてReflectionよりも優れています(ただし、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);
    }
  }
}
16
Holger

JDK 7および8のMethodHandlesにはcatch 22があります(JDK 9以降はまだテストしていません):MethodHandleは高速です(静的フィールドにある場合は直接アクセスと同じくらい高速です。それ以外の場合は反射と同じくらい低速です。フレームワークがn個のゲッターまたはセッターを反映している場合(コンパイル時にnが不明)、MethodHandlesはおそらく役に立たないでしょう。

私は リフレクションをスピードアップするためのすべての異なるアプローチをベンチマークした記事 を書きました。

LambdaMetafactory(またはコード生成などのよりエキゾチックなアプローチ)を使用して、ゲッターとセッターの呼び出しを高速化します。ゲッターの要点は次のとおりです(セッターの場合は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);
    }

}
5

[〜#〜] edit [〜#〜] holgerのおかげで、invokeExactを使用する必要があることに気付いたので、他のjdkに関するものを削除し、invokeExactのみを使用することにしました...サーバーかどうかはまだ私にとって実際には違いはありません

リフレクションを使用する場合と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で45000msのコンピューター時間を使用します(jdk8および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はおよそ1288msを言います。だから私は7u40でホルガーの30回を確認することができます。 7u06では、リフレクションが数倍速く、jdk8ではすべてが再び新しいため、このコード処理は遅くなります。

改善が見られなかった理由については...言うのは難しいです。私がしたのはマイクロベンチマークでした。それは実際のアプリケーションについては何も教えてくれません。しかし、これらの結果を使用すると、古いjdkバージョンを使用するか、ハンドルを十分に再利用しないと思います。ハンドルの実行は高速になる可能性がありますが、ハンドルの作成はフィールドの作成よりもはるかにコストがかかる可能性があるためです。

今最大の問題点...私はあなたがグーグルアプリエンジンのためにこれを望んでいるのを見ました...そして私は言わなければなりません、あなたはあなたが望むだけローカルでテストすることができます、最終的に重要なのはグーグルでのアプリケーションのパフォーマンスですサイトになります。 Afaikは、変更されたOpenJDKを使用していますが、どのバージョンでどのような変更が加えられているかについては言及していません。 Jdk7が非常に不安定なため、運が悪いかどうかはわかりません。たぶん彼らはリフレクションのための特別なコードを追加しました、そしてとにかくすべての賭けはオフです。そしてそれを無視しても...おそらく支払いモデルが再び変更されましたが、通常はコストがかかるため、キャッシュによるデータストアアクセスを避けたいと考えています。それでも問題が解決しない場合、ハンドルが平均10.000回呼び出されるのは現実的ですか?

4
blackdrag