convertir - Mejora de campo para obtener y establecer el rendimiento con ASM o Javassist
convertir codigo java a ensamblador (5)
Me gustaría evitar la reflexión en un proyecto de código abierto que estoy desarrollando. Aquí tengo clases como las siguientes.
public class PurchaseOrder {
@Property
private Customer customer;
@Property
private String name;
}
Escaneo la anotación @Property
para determinar qué puedo establecer y obtener de PurchaseOrder de manera reflexiva. Hay muchas clases de este tipo que usan java.lang.reflect.Field.get()
y java.lang.reflect.Field.set()
.
Idealmente, me gustaría generar para cada propiedad un invocador como el siguiente.
public interface PropertyAccessor<S, V> {
public void set(S source, V value);
public V get(S source);
}
Ahora, cuando escaneo la clase, puedo crear una clase interna estática de PurchaseOrder
como tal.
static class customer_Field implements PropertyAccessor<PurchaseOrder, Customer> {
public void set(PurchaseOrder order, Customer customer) {
order.customer = customer;
}
public Customer get(PurchaseOrder order) {
return order.customer;
}
}
Con estos evito totalmente el costo de la reflexión. Ahora puedo configurar y obtener desde mis instancias con rendimiento nativo. ¿Alguien puede decirme cómo haría esto? Un ejemplo de código sería genial. He buscado en la red un buen ejemplo pero no puedo encontrar nada como esto. Los ejemplos ASM y Javasist son bastante pobres también.
La clave aquí es que tengo una interfaz que puedo pasar. Entonces puedo tener varias implementaciones, tal vez una con Java Reflection por defecto, una con ASM y otra con Javassist.
Cualquier ayuda sería muy apreciada.
El objetivo es el rendimiento!
Sí, eso es en muchos casos el objetivo. ¡Pero lo que estás haciendo ahora con PropertyAccessor es que tu rendimiento baja! Cada vez que desee obtener o establecer una propiedad, deberá crear una nueva instancia para customer_Field
. O tienes que mantener tu instancia. No veo cuál es el problema con un getter o setter simple.
public class PurchaseOrder {
@Property
private Customer customer;
@Property
private String name;
pulic void setCustomer(Customer c)
{
this.customer = c;
}
public Customer getCustomer()
{
return customer;
}
// The same for name
}
¡Este es el rendimiento! El código nativo es tal vez 14 veces más rápido, pero ¿realmente lo necesita tan rápido? Java es genial. ¿Por qué? Por su independencia de plataforma. Y si vas a hacer cosas nativas, el poder de Java se ha ido. Entonces, ¿cuál es la diferencia entre esperar un minuto por hacer todo lo que los programas tienen que hacer y esperar 50 segundos? "¿Dónde está mi 14 veces más rápido?" No solo necesita obtener y establecer. Necesitas hacer algo con todos los datos.
Y no creo que sea más rápido, porque acaba de obtener y configurar instancias de objetos y elementos primitivos. Native Java se crea para:
- métodos que tienen que calcular algo que sería realmente más rápido en código de máquina que con Java Runtime Environment (Muchos métodos
java.lang.Math
, comosqrt()
. Podrían programarlo en Java, pero sería más lento) - cosas que Java no puede hacer por sí mismo, como: salir de la aplicación, crear sockets, escribir / leer archivos, invocar otros procesos, etc. ... Eso no es puro Java, que es código de máquina nativo que sí lo hace.
Así que espero haberte persuadido y lo mantendrás en Java.
Me sorprende que la reflexión sea mucho más lenta. Si calienta la JVM, no debe ser más de 5 veces más lenta que el acceso directo. Por cierto, un micro-benchmark puede dar resultados erróneos porque un getter / setter simple puede optimizarse fácilmente en nada si no funciona bien.
Otra forma de evitar la reflexión y el código de bytes es usar la clase sun.misc.Unsafe. Tiene que manejarse con cuidado y no es portátil para todas las JVM, pero es 2-3 veces más rápido que la reflexión. Vea mi proyecto essence-rmi para ejemplos.
Otra opción es generar código y compilarlo sobre la marcha. Puede usar la API del compilador o una biblioteca como BeanShell.
Nota: si tiene un campo privado, no se puede acceder desde otra clase usando el código de bytes. Esta es una restricción JVM. Las clases internas y anidadas evitan esto generando métodos de acceso para usted, como acceso $ 100 en la clase con los métodos privados (puede haberlos visto en su pila de llamadas). Sin embargo, significa que no puede agregar una clase para acceder a campos privados sin alterar el original clase.
También puede utilizar los Procesadores de anotación, evitando así la complejidad de la manipulación de bytecode. (mira este artículo en javabeat )
Un ejemplo de uso de Javassist, sin embargo, requiere que sus propiedades tengan protección de nivel de paquete en lugar de ser privadas.
public class AccessorGenerator {
private final ClassPool pool;
public PropertyGenerator() {
pool = new ClassPool();
pool.appendSystemPath();
}
public Map<String, PropertyAccessor> createAccessors(Class<?> klazz) throws Exception {
Field[] fields = klazz.getDeclaredFields();
Map<String, PropertyAccessor> temp = new HashMap<String, PropertyAccessor>();
for (Field field : fields) {
PropertyAccessor accessor = createAccessor(klazz, field);
temp.put(field.getName(), accessor);
}
return Collections.unmodifiableMap(temp);
}
private PropertyAccessor createAccessor(Class<?> klazz, Field field) throws Exception {
final String classTemplate = "%s_%s_accessor";
final String getTemplate = "public Object get(Object source) { return ((%s)source).%s; }";
final String setTemplate = "public void set(Object dest, Object value) { return ((%s)dest).%s = (%s) value; }";
final String getMethod = String.format(getTemplate,
klazz.getName(),
field.getName());
final String setMethod = String.format(setTemplate,
klazz.getName(),
field.getName(),
field.getType().getName());
final String className = String.format(classTemplate, klazz.getName(), field.getName());
CtClass ctClass = pool.makeClass(className);
ctClass.addMethod(CtNewMethod.make(getMethod, ctClass));
ctClass.addMethod(CtNewMethod.make(setMethod, ctClass));
ctClass.setInterfaces(new CtClass[] { pool.get(PropertyAccessor.class.getName()) });
Class<?> generated = ctClass.toClass();
return (PropertyAccessor) generated.newInstance();
}
public static void main(String[] args) throws Exception {
AccessorGenerator generator = new AccessorGenerator();
Map<String, PropertyAccessor> accessorsByName = generator.createAccessors(PurchaseOrder.class);
PurchaseOrder purchaseOrder = new PurchaseOrder("foo", new Customer());
accessorsByName.get("name").set(purchaseOrder, "bar");
String name = (String) accessorsByName.get("name").get(purchaseOrder);
System.out.println(name);
}
}
ASM
Usando ASMifierClassVisitor
, puede ver exactamente qué código necesita escribir para generar las clases internas:
ASMifierClassVisitor.main(new String[] { PurchaseOrder.customer_Field.class
.getName() });
El resto es solo determinar qué bits necesita parametrizar en el código de su generador. Ejemplo de salida para PurchaseOrder$customer_Field
que se convertirá en el archivo inject/PurchaseOrder$customer_Field.class
:
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_6, ACC_SUPER, "inject/PurchaseOrder$customer_Field",
"Ljava/lang/Object;"+
"Linject/PropertyAccessor<Linject/PurchaseOrder;Linject/Customer;>;",
"java/lang/Object",
new String[] { "inject/PropertyAccessor" });
//etc
(Utilicé "inyectar" como el paquete).
También deberá crear accesos sintéticos utilizando las clases de visitante de ASM:
{
mv = cw.visitMethod(ACC_STATIC + ACC_SYNTHETIC, "access$0",
"(Linject/PurchaseOrder;Linject/Customer;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, "inject/PurchaseOrder",
"customer", "Linject/Customer;");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_STATIC + ACC_SYNTHETIC, "access$1",
"(Linject/PurchaseOrder;)Linject/Customer;", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "inject/PurchaseOrder", "
customer", "Linject/Customer;");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
Vea este proyecto para ver un ejemplo de cómo inyectar métodos.
Con estos evito totalmente el costo de la reflexión.
Como todo esto se hará en tiempo de ejecución:
- hay un costo inicial para este análisis sintáctico y generación de código
- tendrá que descubrir e introspectar estos tipos generados de alguna manera