miércoles, julio 08, 2009

Tipos anónimos, Expresiones Linq y APIs fluidas

Hace ya una semana a través de twitter me llegó me llego un post (Using C# 3.0 Anonymous Types as Dictionaries) que presentaba una propuesta para usar tipos anónimos como diccionarios. Para seguir les recomiendo ver el post pues asumo que se conoce el contexto del problema

El post genero muchas reacciones y comentarios, que pueden verse en el mismo post, personalmente me gustó mucho como se ve el código y me hizo recuerdo a las notaciones JSON, o bien a como se harían estas cosas en lenguajes dinámicos, pero como dije en twitter pero siempre está la duda ¿rendimiento?

Luego en la semana me puse a pensar de nuevo en esto y se me ocurrió que este caso en particular .net 3.5 tenía otra caracteristica que podía ser de ayuda Expresiones Linq (para referencia Árboles de expresiones). Las expresiones no solo nos permiten navegar en declaraciones para distintos fines sino que además nos permiten crear y compilar funciones Lambda. Esto me parecía interesante porque dado un tipo cualquiera esposible crear una función Lambda que la procesara y generará la cadena de atributos completa.

Sin embargo, por ahora lambda expressiones siempre terminan en una sola expresión y no es posible construir todo un método de esta manera. Y es aqui donde las APIs fluidas (también conocidas como interfaces fluidas, Fluent API, Fluent Interfaces) entran en juego, Las APIs Fluidas brindan, a través de patrones como encadenamiento de métodos, la posibilidad de escribir código de manera consecuente y asegurando que el contexto en la ejecución se mantenga. Por ejemplo todos los métodos Append de la clase StringBuilder, retornan el objeto StringBuilder lo que permite escribir código de este tipo:

StringBuilder sb= new StringBuilder;
sb.Append("Hola").Append(" ").Append("mundo");

Dado que en este caso el principal objetivo es generar una cadena con un StringBuilder, la interfaz Fluida es de mucha utilidad pues se puede generar una sola expresión que agregue todos los atributos al elemento. Esto podía tener un impacto positivo en el rendimiento (mi preocupación del momento).


Así que decidí hacer un ejercicio. Tomé el código de Eilon e hice dos nuevas implementaciones de GetHtmlLink.


GetHtmlLinkDictionary: Que recibe un diccionario y que utiliza inicializadores para el paso de parámeros


GetHtmlLinkLambda: Esta es la que implementa la idea que tenía en la cabeza. Esta función recibe una clase cualquier y para cada propiedad pública de la clase genera un atributo en el vínculo html que genera.


Para ello primero es necesario obtener la información de las propiedades de la clase, luego generar la expresión que agrega los atributos y finalmente compilarla. Como la idea de esta prueba es que la expresión compilada sea utilizada en distintas llamadas cree un diccionario simple para que actue como repositorio de las funciones precompiladas. Con esto en mente el flujo básico de la función es el siguiente:


image


Y la implementación base es la siguiente.


static IDictionary<Type, Action<StringBuilder, object>> functionCache = new Dictionary<Type, Action<StringBuilder, object>>();
public static string GetHtmlLinkLamba(string text, object properties)
{
StringBuilder sb = new StringBuilder();
sb.Append("<a");
if (!functionCache.ContainsKey(properties.GetType()))
{
functionCache.Add(properties.GetType(), BuildFunction(properties))
}
functionCache[properties.GetType()](sb, properties);
sb.Append(">");
sb.Append(text);
sb.Append("</a>");
return sb.ToString();
}


Como notarán el delegado que se genera recibe dos parámetros la clase StringBuilder y el objeto que se utilizará. Un aspecto que no pude optimizar es el paso del objeto de manera no tipada esto debido a que para poder invocar las funciones precompiladas es necesario conocer la firma del delegado con anticipación y eso incluye el tipo del parámetro (este es un tema pendiente a resolver).


Obviamente la parte jugosa está en la función BuildFunction. Lo primero, en mi caso, fue ordenar las ideas de la construcción de la expresión. Mi primer enfoque fue construir una expresión que hiciera lo mismo que la función que carga la información del diccionario y dado que el número de propiedades es variable necesito que mi expresión vaya agrengadose. Es decir que si hay solo la propiedad class la expresión generada sea la siguiente:


sb.Append(" ").Append(“class”).Append("=\"").Append(obj.class).Append("\"")

Y si hay una propiedad adicional target la expresión pase a ser:


sb.Append(" ").Append(“class”).Append("=\"").Append(obj.class).Append("\" ").Append(“target”).Append("=\"").Append(obj.target).Append("\"")

Esta alternativa la llegué a implementar, pero luego me di cuenta, lento que fuí, que al conocer toda la infomración de la clase de antemano podía generar una expresión mucho más óptima.


sb.Append(" class =\"").Append(obj.class).Append("\"  target=\"").Append(obj.target).Append("\"")

Esta "realización" simplifico mucho mi código y ese es el que presento. Lo primero que tuve que crear fueron expresiones que representen lo parámetros que la función recibiría, para ello cree expresiones del tipo ParameterExpression:


var paramSb = ParameterExpression.Parameter(typeof(StringBuilder), "sb");
var paramObj = ParameterExpression.Parameter(typeof(object), "obj");

Luego tuve que crear el primer elemento de mi expresión que es simplemente la referencia al objeto StringBuilder.


Expression body = paramSb;

Claramente si quería acceder a las propiedades del objeto era necesario que hiciera un cast al tipo particular del objeto, para esto cree una expresión UnaryExpression utilizando el método Convert


var convert = Expression.Convert(paramObj, anonymousType);

Con estos elementos dispuestos podía empezar a iterar a través de las propiedades de la clase. Para ello tuve que recurrir a Reflection (gulp!) y en este punto me di cuenta que para poder crear la expresión de invocación también necesitaba la información de los métodos a los que deseaba invocar. Así que estas lineas son Reflection y nada más.


MethodInfo appendString = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(string) });
MethodInfo appendObject = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(object) });
PropertyInfo[] props = anonymousType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);

Con todo listo armé el bloque que recorre todas las propiedades y va generando la expresión


bool first = true;           
foreach (PropertyInfo prop in props)
{
body =
Expression.Call(
Expression.Call(
body,
appendString,
Expression.Constant((first ? " ":"\" ") + prop.Name + "=\"")),
appendObject,
Expression.Property(convert, prop));
first=false;
}

Paso a explicar el bloque, lo primero que hage es crear una llamada al método Append de la última expresión (que siempre es un StringBuilder gracias a la interfaz fluida) y paso como parámetro el nombre de la propiedad seguida del igual y apertura de comillas (Hay una diferenciación para la primera ejecución que solo genera el nombre). Esta acción corresponde a la expresión anidada.


Expression.Call(
body,
appendString,
Expression.Constant((first ? " ":"\" ") + prop.Name + "=\""))


Luego agrego una llamada al método Append de la clase StringBuilder, pero esta vez a la implementación que recibe un objeto como parámetro, pasando como parámetro el objeto ya tipado por la conversión
(potencialmente podría optimizarse esta generación para diferenciar las propiedades que son cadena o no)


Expression.Call(
/*...*/,
appendObject,
Expression.Property(convert, prop))


La expresión resultante es asignada para que se la base de la próxima itearción. Una vez que se hayan procesado todas las propiedades solo queda agregar el cierre de comillas (si es que se proceso al menos un atributo)



 if (!first)
{
body = Expression.Call(
body,
appendString,
Expression.Constant("\""));
}

Y el toque final la compilación, en este caso estoy especificando el formato del delegado a generar para que pueda incluirlo en el diccionario de funciones


return Expression.Lambda<Action<StringBuilder, object>>(body, paramSb, paramObj).Compile()

Listo, ya tenemos una función que procesa el objeto de manera tipada. luego de organizar y para evitar creaciones innecesarias la estructura final de la función es la siguiente:


        static MethodInfo appendString = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(string) });
static MethodInfo appendObject = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(object) });

private static Action<StringBuilder, object> BuildFunction(object properties)
{
Type anonymousType = properties.GetType();
var paramSb = ParameterExpression.Parameter(typeof(StringBuilder), "sb");
var paramObj = ParameterExpression.Parameter(typeof(object), "obj");
var convert = Expression.Convert(paramObj, anonymousType);
Expression body = paramSb;
PropertyInfo[] props = anonymousType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);
bool first = true;
foreach (PropertyInfo prop in props)
{
body =
Expression.Call(
Expression.Call(
body,
appendString,
Expression.Constant((first ? " ":"\" ") + prop.Name + "=\"")),
appendObject,
Expression.Property(convert, prop));
first=false;
}
if (!first)
{
body = Expression.Call(
body,
appendString,
Expression.Constant("\""));
}
return Expression.Lambda<Action<StringBuilder, object>>(body, paramSb, paramObj).Compile();
}


Hecha la implementación llegó el momento de medirla. Lo que hice fue medir el tiempo de la primera ejecución y el promedio de subsecuentes ejecuciones para cada implementación. La razón para medir de esta forma es que el ejemplo esta orientado a una página Web donde cada ejecución no iniciaría de nuevo la función y solo el primer acceso sería penalizado por la compilación (similar a lo que hace ASP.Net para sitios que no están precompilados).


Las llamadas ejecutadas son las siguientes


HtmlHelpers.GetHtmlLink("Ejemplo", new { @class = "style" , target ="top" });
HtmlHelpers.GetHtmlLinkDictionary("Ejemplo", new Dictionary {{"class", "style"}, {"target", "top"}});
HtmlHelpers.GetHtmlLinkLamba("Ejemplo", new { @class = "style" , target ="top" }));

He aqui los resultados.


Función Primera Ejecución Promedio Ejecuciones Posteriores
GetHtmlLink 15195 2
GetHtmlLinkDictionary 7561 36
GetHtmlLinkLambda 12032 2

Como era de esperarse la implementación inicial es la que menor rendimiento tiene en todos los casos así que no la revisaré. Lo que quiero rescatar es que si bien la primera ejecución del método que recibe el diccionario es más rápida (estos son ticks lo que significa que es medio segundo más rápida) posteriores ejecuciones son mucho más rápidas pues la expresión compilada no necesita hacer ningún tipo de recorrido o búsqueda.


Con esos resultado, y para un sitio web o servicio donde el proceso se mantiene en memoria para atender múltiples requerimientos, me quedaría con la opción de precompilar y generar un código más agradable a la vista

No hay comentarios.: