Как я могу улучшить производительность Field.set (perhap используя MethodHandles)?
Я пишу код, который вызывает Field.set
а также Field.get
много много тысяч раз. Очевидно, это очень медленно из-за отражения.
Я хочу посмотреть, смогу ли я улучшить производительность, используя MethodHandles
в 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);
Тем не менее, это не выглядит лучше, чем вызов Field.set с использованием отражения. Я что-то здесь не так делаю?
Я прочитал это, используя invokeExact
может быть быстрее, но когда я попытался использовать это, я получил java.lang.invoke.WrongMethodTypeException
Кто-нибудь успешно смог оптимизировать повторные звонки в Field.set или Field.get?
4 ответа
2015-06-01: Обновлено, чтобы отразить комментарий @JoeC о другом случае, когда дескрипторы статичны. Также обновлен до последней версии JMH и перезапущен на современном оборудовании. Вывод остается практически неизменным.
Пожалуйста, сделайте правильный бенчмаркинг, возможно, это не так сложно с JMH. Как только вы это сделаете, ответ станет очевидным. Это также может продемонстрировать правильное использование invokeExact
(требует target/source 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_*
случаи, когда вызывающие статически известны.
Обратите внимание, что отражающая производительность находится на одном уровне с MethodHandles в dynamic_*
в некоторых случаях это связано с тем, что рефлексия в JDK 8 дополнительно оптимизирована (поскольку на самом деле вам не нужна проверка доступа для чтения ваших собственных полей), поэтому ответом может быть "просто" переключиться на JDK 8;)
static_*
случаи еще быстрее, потому что MethoHandles.invoke
звонки агрессивно встроены. Это исключает часть проверки типов в случаях MH. Но в случаях отражения все еще присутствуют быстрые проверки, и, следовательно, они отстают.
Обновление: поскольку некоторые люди начали бессмысленную дискуссию о том, "как оценивать", я остановлюсь на решении вашей проблемы, которое содержится в моем ответе, теперь прямо в начале:
Ты можешь использовать invokeExact
даже в вашем рефлексивном контексте, где у вас нет точной сигнатуры типа путем преобразования MethodHandle
с помощью asType
чтобы взять ручку Object
в качестве аргументов. В средах, на которые влияет разница в производительности между invoke
а также invokeExact
, с помощью invokeExact
на такой конвертирующей ручке все еще намного быстрее чем invoke
на прямом методе управления.
Оригинальный ответ:
Проблема действительно в том, что вы не используете invokeExact
, Ниже приведена небольшая тестовая программа, показывающая результаты различных способов увеличения int
поле. С помощью invoke
вместо invokeExact
приводит к падению производительности ниже скорости отражения.
Вы получаете WrongMethodTypeException
поскольку MethodHandle
строго типизирован. Он ожидает точную сигнатуру вызова, соответствующую типу поля и владельца. Но вы можете использовать ручку, чтобы создать новый MethodHandle
упаковка необходимых преобразований типов. С помощью invokeExact
на этой ручке, используя общую подпись (т.е. (Object,Object)Object
) будет по-прежнему более эффективным, чем использование invoke
с динамическим преобразованием типов.
Результаты на моей машине с использованием 1.7.0_40 были:
прямой: 27 415 нс отражение: 1088 462 нс дескриптор метода: 7133,221 нс mh invokeExact: 60 928 нс общий mh: 68,025 нс
и используя -server
JVM уступает
прямой: 26 953 нс отражение: 629,161нс дескриптор метода: 1513,226 нс mh invokeExact: 22,325 нс общий mh: 43 608 нс
Я не думаю, что это имеет большое значение для реальной жизни, видя MethodHandle
быстрее, чем прямая операция, но это доказывает, что MethodHandle
S не медленный на 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);
}
}
}
РЕДАКТИРОВАТЬ благодаря 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");
}
}
}
Тогда я получаю на моем компьютере раз 45000ms на jdk7u40 (хотя 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мс. Так что я могу подтвердить 30 раз Хольгера на 7u40. На 7u06 этот код обрабатывает медленнее, потому что отражение было в несколько раз быстрее, а на jdk8 все снова новое.
Что касается того, почему вы не видели улучшения... трудно сказать. То, что я сделал, было микробенчмаркинг. Это ничего не говорит о реальном приложении вообще. Но, используя эти результаты, я бы предположил, что вы либо используете старую версию jdk, либо недостаточно часто используете дескриптор. Поскольку выполнение дескриптора может быть быстрее, создание дескриптора может стоить намного дороже, чем создание поля.
Теперь самая большая проблема... Я видел, что вы хотите это для Google Appengine... И я должен сказать, что вы можете тестировать локально столько, сколько хотите, что в итоге имеет значение, как производительность приложения в Google сайт будет. Afaik они используют модифицированный OpenJDK, но какую версию с какой модификацией они не говорят. Если Jdk7 нестабилен, вам может не повезти или нет. Может быть, они добавили специальный код для отражения, тогда все ставки в любом случае отключены. И даже игнорируя это... возможно, модель оплаты снова изменилась, но обычно вы хотите избежать доступа к хранилищу данных путем кэширования, потому что это стоит. Если это все еще имеет место, то реально ли, что любой дескриптор будет вызван, скажем, в среднем 10.000 раз?
Есть уловка 22 для MethodHandles в JDK 7 и 8 (я еще не тестировал 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);
}
}