17 marzo 2008

Carga de cuentas de un ejercicio

Tenemos un pequeño problema para la carga de las cuentas contables de un ejercicio concreto, sobre todo al cargar las auxiliares (ya que hay más y el rendimiento debe ser mejor). Os cuento primero cómo cargamos (identificamos) las cuentas de un ejercicio, y a continuación la problemática que veo y la que creo que es mejor solución.
En la nueva contabilidad, como en la misma base de datos mantenemos distintos ejercicios, puede haber cuentas de este ejercicio y otras que, o bien estén ya de baja, o pertenezcan a ejercicios futuros. Antes que nada, ¿qué entendemos por ejercicio actual? Hemos llegado al consenso de que no existirá un ejercicio actual global, pero sí lo habrá en cada contexto: mientras creamos un asiento, por su fecha obtendremos un ejercicio; al visualizar el plan contable, habrá que indicar (o al menos poder cambiar) qué ejercicio queremos ver.
Pues bien, toda cuenta (mayor o auxiliar) tendrá un ejercicio de alta y otro de baja (este último opcional). Al solicitar las cuentas de un ejercicio, deberán mostrarse las que estén de alta en ese ejercicio, lo cual no es un criterio cómodo, no por su complejidad sino porque requiere más datos.
En SQL requiere el enlace de ambos campos con la tabla de ejercicio, por lo que la SQL implicará ya 3 tablas (cuentas + 2 x ejercicios). Esto se debe a que los ejercicios de alta y baja no serán un año ni una fecha, sino el propio ejercicio (su Id en la base de datos), para permitir así contar con ejercicios que abarquen un periodo distinto al año natural (como una temporada deportiva, un año escolar...)
En principio, para cada cuenta se cargarán 2 ejercicios adicionales. Aunque en la Dal aprovechemos la caché de entidades, y estos ejercicios no deban volver a instanciarse, sus datos sí vendrán en la consulta, y el motor de base de datos se habrá preocupado de enviarlos tras comprobar el criterio para el que son necesarios.
Así pues, se me ocurre otra forma de afrontar este problema que creo que tendrá ciertas ventajas, aunque lo dejo a debate abierto. Pienso en no incluir el filtro de altas y baja en la colección, de forma que al componer la SQL no se incluya, por lo que tampoco se necesitarán las 2 tablas de ejercicios (salvo que se hagan visibles como columnas alguna de esas 2 propiedades, lo que no será por defecto). La comprobación de las cuentas de alta en un ejercicio se haría en memoria, con poco esfuerzo ya que EEjercicio es una entidad ideal para ser cacheada, por lo que no tendría que buscar nuevos datos más que en memoria. Sólo tendríamos que eliminar ciertas instancias de la colección (aquellas que comprobemos que no están de alta). Una primera aproximación consiste en crear una colección temporal que carguemos y que recorriéndola compongamos una colección definitiva con las entidades que pasen el criterio. Pero se puede proponer otro enfoque más directo: interceptar el evento AddingNew de las colecciones, y cancelar las adiciones que no consideremos apropiadas.
Instintivamente me atrae más esta aproximación que la del filtro complejo, aunque tiene la contrapartida de que se envían como resultado de la consulta SQL más cuentas de las necesarias, para que luego sean descartadas. Esto sólo es admisible en casos como este, donde la probabilidad de una baja es escasa.
Por otro lado, la alternativa propuesta puede parecer muy cercana a los filtros en memoria. Efectivamente, sería aplicar el filtro de alta a la colección una vez cargada en memoria (no se tome esto literalmente, sería como en el AddingNew, para poder ir filtrando durante una carga asíncrona). La verdad es que me parecería interesante, pero no abordamos esta posibilidad en el diseño original, a diferencia de XPO que ofrece dos propiedades (ambas textuales): Filter (en memoria) y Criteria (en SQL). La idea es que se decidiera siempre dinámicamente dónde aplicar cada filtro. Para poder efectuar el filtro de alta en memoria, debería indicarse explícitamente al crear o añadir el filtro a la colección antes de efectuar la carga, y esto sí es un cambio más profundo que pensaremos para el futuro.

Etiquetas: ,

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: