Почему аннотации под Android такие проблемы с производительностью (медленно)?
Я ведущий автор ORMLite, который использует аннотации Java для классов для построения схем баз данных. Большая проблема с производительностью стартапа для нашего пакета - это вызов методов аннотации в Android 1.6. Я вижу такое же поведение до 3.0.
Мы видим, что следующий простой код аннотации невероятно интенсивен для GC и представляет собой реальную проблему производительности. 1000 быстрых обращений к методу аннотации занимают почти секунду на быстром устройстве Android. Один и тот же код, работающий на моем Macbook Pro, может одновременно выполнять 28 миллионов вызовов. У нас есть аннотация, содержащая 25 методов, и мы хотели бы выполнять более 50 из них в секунду.
Кто-нибудь знает, почему это происходит, и есть ли работа вокруг? Конечно, есть вещи, которые ORMLite может сделать в плане кэширования этой информации, но есть ли что-то, что мы можем сделать, чтобы "исправить" аннотации под Android? Благодарю.
public void testAndroidAnnotations() throws Exception {
Field field = Foo.class.getDeclaredField("field");
MyAnnotation myAnnotation = field.getAnnotation(MyAnnotation.class);
long before = System.currentTimeMillis();
for (int i = 0; i < 1000; i++)
myAnnotation.foo();
Log.i("test", "in " + (System.currentTimeMillis() - before) + "ms");
}
@Target(FIELD) @Retention(RUNTIME)
private static @interface MyAnnotation {
String foo();
}
private static class Foo {
@MyAnnotation(foo = "bar")
String field;
}
Это приводит к следующему выводу журнала:
I/TestRunner( 895): started: testAndroidAnnotations
D/dalvikvm( 895): GC freed 6567 objects / 476320 bytes in 85ms
D/dalvikvm( 895): GC freed 8951 objects / 599944 bytes in 71ms
D/dalvikvm( 895): GC freed 7721 objects / 524576 bytes in 68ms
D/dalvikvm( 895): GC freed 7709 objects / 523448 bytes in 73ms
I/test ( 895): in 854ms
РЕДАКТИРОВАТЬ:
После того, как @candrews указал мне правильное направление, я немного поковырялся в коде. Проблема производительности, похоже, вызвана каким-то ужасным, грубым кодом в Method.equals()
, Это зовет toString()
обоих методов, а затем сравнивая их. каждый toString()
использование StringBuilder
с кучей методов добавления без хорошего размера инициализации. Делать .equals
при сравнении полей будет значительно быстрее.
РЕДАКТИРОВАТЬ:
Интересное улучшение производительности отражения было дано мне. Теперь мы используем отражение, чтобы заглянуть внутрь AnnotationFactory
класс для чтения списка полей напрямую. Это делает класс отражения в 20 раз быстрее для нас, поскольку он обходит вызов, использующий method.equals()
вызов. Это не общее решение, но вот код Java из репозитория ORMLite SVN. Для общего решения см . Ответ Янченко ниже.
4 ответа
Google признал проблему и исправил ее "после соты"
Так что, по крайней мере, они знают об этом и предположительно исправили это для какой-то будущей версии.
Вот общая версия идеи Грея& user931366:
public class AnnotationElementsReader {
private static Field elementsField;
private static Field nameField;
private static Method validateValueMethod;
public static HashMap<String, Object> getElements(Annotation annotation)
throws Exception {
HashMap<String, Object> map = new HashMap<String, Object>();
InvocationHandler handler = Proxy.getInvocationHandler(annotation);
if (elementsField == null) {
elementsField = handler.getClass().getDeclaredField("elements");
elementsField.setAccessible(true);
}
Object[] annotationMembers = (Object[]) elementsField.get(handler);
for (Object annotationMember : annotationMembers) {
if (nameField == null) {
Class<?> cl = annotationMember.getClass();
nameField = cl.getDeclaredField("name");
nameField.setAccessible(true);
validateValueMethod = cl.getDeclaredMethod("validateValue");
validateValueMethod.setAccessible(true);
}
String name = (String) nameField.get(annotationMember);
Object val = validateValueMethod.invoke(annotationMember);
map.put(name, val);
}
return map;
}
}
Я сравнил аннотацию с 4 элементами.
Время в миллисекундах для 10000 итераций либо получения значений всех из них, либо вызова метода выше:
Device Default Hack
HTC Desire 2.3.7 11094 730
Emulator 4.0.4 3157 528
Galaxy Nexus 4.3 1248 392
Вот как я интегрировал его в DroidParts: https://github.com/yanchenko/droidparts/commit/93fd1a1d6c76c2f4abf185f92c5c59e285f8bc69.
Чтобы решить эту проблему, здесь все еще есть проблема при вызове методов для аннотаций. Ошибка, указанная выше в candrews, исправляет медлительность getAnnotation(), но вызов метода для аннотации по-прежнему является проблемой из-за проблем Method.equals().
Не удалось найти отчет об ошибке для Method.equals(), поэтому я создал его здесь: https://code.google.com/p/android/issues/detail?id=37380
Редактировать: Так что моя работа вокруг этого (спасибо за идеи @Gray), на самом деле довольно проста. (это транкинговый код, некоторое кеширование и прочее опущено)
annotationFactory = Class.forName("org.apache.harmony.lang.annotation.AnnotationFactory");
getElementDesc = annotationFactory.getMethod("getElementsDescription", Class.class);
Object[] members = (Object[])getElementDesc.invoke(annotationFactory, clz); // these are AnnotationMember[]
Object element = null;
for (Object e:members){ // AnnotationMembers
Field f = e.getClass().getDeclaredField("name");
f.setAccessible(true);
String fname = (String) f.get(e);
if (methodName.equals(fname)){
element = e;
break;
}
}
if (element == null) throw new Exception("Element was not found");
Method m = element.getClass().getMethod("validateValue");
return m.invoke(element, args);
Ваш пробег будет варьироваться в зависимости от использования, но в некоторых случаях это было примерно в 15-20 раз быстрее, чем при правильном подходе.
Я думаю, что если вам удастся изменить политику хранения RUNTIME, она не должна быть такой медленной.
РЕДАКТИРОВАТЬ: Я знаю, для вашего проекта это может быть не вариант. Возможно, это скорее проблема того, что вы делаете с этой аннотацией, чем плохая производительность в целом.