05 marzo 2008

Rendimiento de Activator.CreateInstance

Actualmente N2 utiliza mucho Activator.CreateInstance internamente, tanto en la carga de colecciones como en la carga de una entidad individual. Sólo se usa new E() para crear la entidad, pero para cargar sus propiedades hijas se utiliza Activator. Yo tenía la sospecha desde hace tiempo de que el rendimiento debía ser peor, y la he confirmado con el artículo de Haibo Luo.

El artículo, para empezar, es acojonante, sobre todo para los que estamos tan lejos de las profundidades de los compiladores... sobre todo cuando se mete a utilizar Emit para producir código en el OPCODE de .net. Pero lo bonito de estas cosas son siempre los números, así que fijaos en la tabla de tiempos obtenidos creando 1000 instancias de distintas clases de objetos usando los distintos métodos. Baja de los 9 segundos de Activator a las 5 centésimas usando DynamicMethod: 180 veces más rápido. En mi máquina, las diferencias son mucho mejores, porque es mucho más lenta y porque he probado con 2000 en vez de 1000:

ParameterAndActivator 00:00:00 - 00:03:10.8803610 -> 3,10 minutos
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'inmemory', no se pueden cargar símbolos.
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'helper', no se pueden cargar símbolos.
ParameterAndEmit 00:00:01.0770795 - 00:00:56.9651040
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'inmemory', no se pueden cargar símbolos.
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'helper', no se pueden cargar símbolos.
ParameterAndEmitAndDelegate 00:00:01.1395755 - 00:00:01.5223635
ParameterAndDynamicMethod 00:00:00.1142505 - 00:00:39.5804745
ParameterAndDynamicMethodAndDelegate 00:00:00.1699110 - 00:00:01.6698150
ParameterAndCtorInfo 00:00:00.0751905 - 00:00:11.5490655

He destacado el método ParameterAndDynamicMethodAndDelegate porque es el más simple de usar (ver el artículo), y además da un tiempo muy muy bueno. Pero ojo, estas pruebas son para un constructor con 1 parámetro. Sorprendentemente, si probamos con un constructor sin parámetros los tiempos son otros:

ParameterlessAndActivator 00:00:00 - 00:00:16.8407190
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'inmemory', no se pueden cargar símbolos.
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'helper', no se pueden cargar símbolos.
ParameterlessAndEmit 00:00:01.0692675 - 00:00:50.2370190
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'inmemory', no se pueden cargar símbolos.
'Nibi.Negocio.Prueba.vshost.exe' (administrado): se cargó 'helper', no se pueden cargar símbolos.
ParameterlessAndEmitAndDelegate 00:00:01.1327400 - 00:00:01.4657265
ParameterlessAndDynamicMethod 00:00:00.1122975 - 00:00:28.5274710
ParameterlessAndDynamicMethodAndDelegate 00:00:00.1601460 - 00:00:01.6258725
ParameterlessAndCtorInfo 00:00:00.0732375 - 00:00:08.4506310

Los tiempos son mucho mejores en todos los casos, incluyendo Activator, que mejora mucho cuando el constructor no tiene parámetros. Pero queda claro que el método destacado es mucho mejor, aunque no en todos los escenarios. ¿Por qué? Porque estamos hablando de lotes muy grandes, y en esos el tiempo de preparación se compensa. En cambio, para crear una sola instancia no merecería la pena. Pero podemos suponer que la preparación comentada se almacenará en caché, y que cualquier aplicación va a crear las suficientes instancias de cada clase como para compensar también ese esfuerzo inicial. Además, la preparación no debe ser para todas las clases como en la prueba, sino que cuando vamos a crear una instancia, si no tenemos su clase preparada, la preparamos (lo que debe tardar mucho menos que la preparación de 2000 tipos, que son los tiempos que tenemos).

Hay que destacar el último párrafo del artículo, donde habla de la limitación a 16 tipos de Activator: si creas 16 tipos, es bastante eficiente: el mejor método sólo lo rebaja a la mitad. Pero si en vez de 16 usas 17 tipos, el tiempo empeora 9 veces, con lo que la distancia es de 18 veces (porque el mejor método escala muy bien, apenas nota el nuevo tipo). Pensado así, es muy poco probable que en N2 se reproduzca el patrón probado: crear en secuencia 17 tipos distintos. En la carga de una colección, por mucho que se profundice, no creo que hayamos llegado a encontrarnos con un caso así, por lo que la mejora de rendimiento no sería tan acusada como puede parecer. Pero una mejora del 50% ya es bastante importante como para que merezca la pena pelearse con Emit(), ¿no?

Etiquetas:

1 Comments:

Blogger Pablo said...

Mas comentarios apoyando este sistema en:
http://blog.hackedbrain.com/archive/2006/09/14/5263.aspx
Comenta que la aproximación de Emit es compleja, pero que en .net 2.0 pueden usarse métodos dinámicos con mayor facilitad.

Mi primera decisión tras todo esto ha sido intentar usar siempre que pueda new T() en vez de Activator. Pero no siempre es posible, ya que para eso T debe tener un constructor, cuando en muchas ocasiones T es abstracta y es internamente donde busco el tipo que voy a instanciar. Yo quería que cuando el tipo a instanciar fuera el propio T (no abstracto ni sustituido), utilizar new T() por eficiencia, pero ya digo, no es posible por la restricción de compilación que impone declarar que T tenga un new(). Así que voy a utilizar Activator.CreateInstance[T](), que según leo en http://www.thescripts.com/forum/thread506085.html es el código generado por el compilador cuando se encuentra new T() en una clase genérica (por lo que no hay diferencia alguna en el rendimiento).
Aunque en la misma discusión se afirma que internamente el método usa su hermano no genérico, del que ya conocemos sus problemas de rendimiento, así que también deberíamos sustituirlo si nos decantamos por cambiar todas las instanciaciones.

11 marzo, 2008 09:59  

Publicar un comentario

<< Home