_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への繰り返しの呼び出しを最適化できた人はいますか?
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の場合の型チェックの一部が不要になります。ただし、リフレクションの場合は、まだクイックチェックが存在するため、遅れています。
更新:「ベンチマークの方法」について無意味な議論を始めた人もいるので、私の答えに含まれている問題の解決策を強調します。始まり:
invokeExact
を使用してMethodHandle
をハンドルに変換することにより、正確な型シグネチャがないリフレクティブコンテキストでもasType
を使用できます。 Object
を引数として取ります。 invoke
とinvokeExact
のパフォーマンスの違いの影響を受ける環境では、このような変換ハンドルで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ではMethodHandle
sが遅くないことを証明しています。
また、汎用の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);
}
}
}
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);
}
}
[〜#〜] 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回呼び出されるのは現実的ですか?