Haanga vs Django vs H2O
En mi apunte anterior conté que habíamos desarrollado y liberado Haanga, el sistema de plantillas de Menéame. Ahora acabo de hacer unos benchmarks comparándolo con Django y H2O. La intención fue medir el rendimiento del motor de plantillas de cada uno.
Los resultados se muestran en la tabla a continuación. La primera columna es el tipo de benchmark. La segunda, tercera y cuarta son el número de conexiones por segundo (más altas son mejores) de cada uno de los sistemas. Las dos últimas columnas indican cuantas veces más conexiones por segundo sirvió Haanga.
| Bench | Haanga | Django | H2O | vs Django | vs H2O |
| b1 (sin plantillas) | 3214.84 | 769.48 | 1447.68 | 4.2 | 2.2 |
| b1_tpl | 1031.57 | 203.28 | 273.24 | 5.1 | 3.8 |
| b2_tpl | 1944.41 | 270.96 | 240.55 | 7.2 | 8.1 |
| b2_filters | 1011.10 | 65.14 | 156.68 | 15.5 | 6.5 |
| header | 2365.59 | 185.69 | 273.61 | 12.7 | 8.6 |
Haanga saca mucha diferencia a los dos sistemas en cualquiera de las pruebas.
Explicación de los benchmarks
- b1 sin plantillas: Este es un programa simple que sólo ejecuta un bucle para imprimir la hora cien veces. No se usan templates, pero sirve para dar una referencia y notar la diferencia entre la inclusión de librerías entre Haanga y H2O (en la vistas de Django ya se incluyen).
- b1_tpl: El mismo resultado que el anterior pero llamándose al generador de plantilla para cada iteración.
- b2_tpl: Este es un caso más habitual. En vez de llamar cien veces al generador se guardan los resultados en un array y al final se genera el resultado desde el template (con el {% for o in objects %}
- b2_filters: Es el mismo programa que el anterior pero al template se le agregaron un par de filtros (La hora ({{ o.i|escape }}) en epochs es: {{o.epoch}} ({{ o.epoch|date:”r” }}).
- header: Este es un caso más real. Tomé una de las plantillas más complejas que usamos en Menéame, la que genera la cabecera HTML, logo y menú superior (header.html). Eliminé las etiquetas no compatibles con los otros dos (los exec y trans), se llama tres veces al generador de plantillas (me aseguré que el HTML era similar en los tres –había diferencia en los espacios en blanco que genera cada uno–).
Los códigos fuentes completos lo podéis bajar en un tgz.
Ejecución
Todas las pruebas fueron hechas sobre un Intel Quad de 2.40GHz con 4GB de RAM. No había otros procesos consumiendo CPU.
Los números de conexiones por segundo fueron obtenidos con el Apache Benchmark contra el servidor local (localhost). El comando fue «ab -q -S -c 2 -t 30 $url», podéis ver el script benchs.sh en el tgz mencionado antes.
Los PHP se ejecutaron en un Apache2 estándar de Ubuntu 10.4, con el PHP5 también estándar, el xcache (con su configuración por defecto). El único cambio que hice fue en la configuración del PHP5 (php.ini) para que sólo avise de errores (i.e. «error_reporting = E_ERROR»).
El Apache con el mod_python me daba resultados muy malos con el Apache 2, así que consulté a Antoni Aloy (un expertísimo en Django) y me recomendó hacerlo con el gunicorn. Este es el comando que utilicé: «python manage.py run_gunicorn -w 5» (el DEBUG del Django estaba deshabilitado).
Actualización: hice las pruebas como me indicó @Rolando con el tornado y su sistema de templates. Estos son los resultados.
| Bench | Tornado |
| b1 (sin plantillas) | 2988.29 |
| b1_tpl | 1111.82 |
| b2_tpl | 1802.91 |
| b2_filters | 613.54 |
| header | 2425.91 |

Gracias por lo de experto
Tal como te comenté además del rendimiento de las plantillas en el caso de Django estamos añadiendo también el tiempo del framework: el paso por los middlewares de Django, configuración etc.
Otra prueba interesante (estoy en ello) es la de generar la plantilla sin pasar por el framework y ver los tiempos de PHP y el sistema de plantillas de Django.
El sistema de plantillas de Django no es super-rápido, es lo suficientemente rápido para que en aplicaciones normales con caché de plantillas y demás pesen más las ventajas de utilizar el sistema de plantillas de Django que el tiempo global de generación del html.
Muy interesante.
Seguramente usando uWSGI con Django aumentarían los req/s, pero no creo que llegase al nivel de Haanga.
¡Enhorabuena!
Utilizar uWSGI o gunicorn es anecdótico en este caso. Propuse gunicorn a Ricardo porque de los motores WSGI es de los más fáciles de integrar con Django.
La compilación de plantillas a PHP es lo que le da a Haanga la velocidad que transmiten las pruebas. Como dije comprobar en req/s es un tanto injusto, aún así las plantillas de Haanga son mucho más rápidas que las de Django para la funcionalidad del benchmarc (eps hay que ser cautelosos, ya que hay mucho bucle).
Si utilizamos sólo la parte de plantillas y descartando la primera ejecución de Haanga, por aquello del tiempo de compilación.
b1: Haanga 3,567s Django 1.3404 para 5 pruebas de un bucle de 10.000. Aquí estamos probando realmente velocidad de PHP vs Python
b1_tpl, también para 10000 iteraciones y 5 ejecuciones:
Haanga: 3,5458s Django 1,925s
b2_tpl, 5 ejecuciones:
Haanga: 0,028s
Django: 0,1358s
b2_1_tpl 5 ejecuciones
Haanga: 0,0312 s
Django: 0,1358 s
header 5 ejecuciones
Haanga: 0,0334 s
Django: 0,1558 s
En este último caso me he podido dar cuenta del porqué la gente de Django (core) es reacia al tema de la compilación. Los mensaje de error de las plantillas de Django son muy informativo, ricos, te indica qué falla en la plantilla y en qué linea del html y/o de la línea Python. En el caso de Haanga los errores son mucho más parcos y hacen referencia al código compilado y no a la plantilla. Para un desarrollador de PHP esto no tiene importancia, pero sí puede tenerla para un maquetador sin conocimientos de programación.
> En el caso de Haanga los errores son mucho más parcos y hacen referencia al código compilado y no a la plantilla.
Haanga sólo mira la plantilla una vez, y si tiene errores te avisa en ese momento. Una vez que compiló sin errores es puro PHP.
El problema que tenemos con PHP5 es que no tiene excepciones de tipo, como el Python, para poner valores por defecto durante la ejecución. Pero si está en modo “normal” de PHP no existen errores de “clave de array no existente”.
@aaloy Aun así, generar código Python desde los templates es relativamente fácil, hasta un _sudaca_ pudo hacerlo en un 1 mes (pero para PHP)
Haanga podría generar código Python (solo tendría que crear lib/Generator/Python.php).
Con respecto a el reporte de errores, lanzo una excepción con todas informaciones (template, linea y lo que fue mal), va mas allá de mis posibilidades hacer un reporte _entendible_ de los errores (la parte gráfica no es mi fuerte
)
@gallir lo de la excepción “por tipos” es una bendición
, lo que no me gusta es que hay que implementar ArrayAccess para que un objeto se acceda como un array (internamente un objeto y un array es un Zend_Hash).
César del reporte de errores me refiero a que cuando te genera un warning en el módulo php no te hace referencia al html original.
He estado comparando también con mako (un template que genera código Python) y los resultados son similares a los de Django. Se supone que Mako es muchísimo más rápido, así que tengo la sensación que estamos probando el caso peor. Los bucles son la parte más floja de Python en cuanto a velocidad.
Leí el código pero no tengo claro si se permite la creación de librerías (los custom template tags) y algunas otras construcciones. Creo que una documentación “a la Django” es el toque final que le falta a esta librería.
Ten por seguro que si tengo que realizar una aplicación en PHP Haanga será mi primera opción como lenguaje de plantillas. Además de que personalmente me estoy divirtiendo mucho con esto
@aaloy
> Leí el código pero no tengo claro si se permite la creación de librerías (los custom template tags) y algunas otras construcciones.
Sí, se puede, es muy fácil. En Menéame tenemos un par (y tendremos más): http://websvn.meneame.net/filedetails.php?repname=meneame&path=/branches/version4/www/libs/haanga_mnm.php
Y sí, la documentación es lo que falta, hay gente que está traduciendo: http://bit.ly/cVLrBJ
@aaloy
Como mencionó @gallir, Haanga solo es utilizado en el *peor* de los casos (cuando el template es mas viejo que su equivalente en PHP, o cuando no existe dicho archivo), al momento de ejecutar solo se carga una pequeña clase que carga al archivo compilado y lo ejecuta, o carga todo el compilador.
Haanga en sí lento (como los demás), porque genera código PHP (tiene estructuras internas mas complejas) ademas porque el lexer y el parser son automáticos (para ports de Lemon Parser y Lex para PHP). Estoy viendo como optimizarlo, una opción valida sería crear el lexer y el parser en C (estuve hablando con @gallir sobre esto). Pero no vale la pena tanto esfuerzo a estas alturas, ya que el *peor* de los casos en menéame es una vez a cada que se actualizan los templates (1 vez por día), que luego cuando terminemos sería *casi* nunca.
Con respecto a las librerías, permite tags y filtros, que están ubicados en lib/Extension/Filter y lib/Extension/Tag. Los filtros pueden ser un alias a una función nativa de PHP, generar código PHP o definir un método que es llamado al tiempo de ejecución (sólo un filtro es así ahora mismo). Los tags pueden ser de bloque o no, definida en la clase ya que el Parser no es muy maleable como el Django.
Puedes fijarte en los fuentes de los tags/filtros y veras que son mas complejas que las de Django, porque generan código PHP.
Los tags complejos son manejados por la clase Haanga_Compiler (en Compiler.php), es decir if, else, for, buffer, regroup, include, base, etc.
Todos los tags/filtros de mnm están aquí: http://websvn.meneame.net/filedetails.php?repname=meneame&path=/branches/version4/www/libs/haanga_mnm.php
Y en cuanto a la documentación estoy trabajando en eso con @gallir y @eddiedean.
Encuentro un poco injusta la comparación de pure PHP + template engine vs full-stack framework.
Django ejecuta middlewares, realiza el matcheo de la url, etc. El template loader en cada request carga el archivo desde disco, a menos que se use el cached Loader. El filtro |date es Haanga es un alias para la función nativa date(), en cambio en Django es un wrapper complejo para emular el formato de PHP.
Pero eso me puse a hacer un port a Tornado, que, hasta donde sé, tiene los templates más rápidos en python.
El código esta en Github, incluyendo los originales en Django y PHP:
http://github.com/darkrho/haanga-benchs/blob/master/tornado_benchs/
Y se salta toda la funcionalidad que no sea importante para estar a “bajo nivel”:
– url routes
– ui modules
– etc, etc, etc
No hace falta correrlo detrás de apache/nginx ya que incluye su propio http server, y por defecto arranca con 5 workers.
Espero que alguien pueda correrlo en un equipo similar al utilizado (4 cores, etc), ya que mi single-core no daría la talla
Saludos desde Sudámerica, vecino de Paraguay, Bolivia!
@rolando Hecho y actualizado el post. Van muy parejos, y dado que el tornado es supermegeficiente, el Apache no tanto (y el Python más eficiente que el PHP), no está nada mal, nada mal.
Gracias por prepararlo.
Yo desde mi ignorancia, tengo una duda. Que diferencia hay entre usar Haanga y usar el propio PHP para las plantillas? Algo como http://www.massassi.com/php/articles/template_engines/
Lo digo porque Haanga guarda el resultado de la plantilla en PHP nativo y realmente eso es lo mismo que se indica en ese articulo no? Ya digo, que es desde la ignorancia, igual coges y lo benchmarkeas (no lo he hecho) y resulta que va lentisimo o algo asi
@gallir tenía mis dudas sobre si igualaría a Haanga
pero no quedo nada mal. Excelente trabajo @crodas.
Una de las cosas que evita a Django ser eficiente es el hecho que los templatetags puedan modificar el contexto cosa que ni Jinja2 ni Tornado permiten. ¿Haanga permite/permitirá eso?
@kr0n Algunas desventajas:
1. El PHP es muy verbosed, tienes que escribir mucho código para hacer poco:
Es muy largo (recurda que <? es algo deprecated, desactivado por defecto, y que se puede confundirse con XML)
2. El cambio de contexto es mas lento que hacer echo (o print), eso realmente importa en sitios grandes.
3. No es amigable con los diseñadores
4. Usar llaves para bloques en entre HTML es muy jodido (muy ilegible). Y no usarlo implica escribir mucho mas, para un loop seria: *
5. ¿Donde está la diversión en usar algo ya existente?
@Rolando Haanga genera código PHP, en si el proyecto Haanga sólo es compilador que tomas los templates y genera un PHP, ademas hace hasta lo imposible para evitar desperdiciar recursos (la pequeña clase que hace todas las verificaciones es lo mas simple posible: http://github.com/crodas/Haanga/blob/master/lib/Haanga.php
Con referencia a los contextos, no tengo bien claro que son
; en Haanga un contexto son las variables que les pasas a la función que luego las convierto a variables locales para que el código evaluado PHP sea mas rápido (internamente todo variable esta es un hash, y los arrays son hashes también, es mejor hacer 1 lookup que 2 lookups). Esto es gracias a http://php.net/extract
La diferencia que vi es que el de Haanga parser es bien estático, con lo justo y necesario para ser rápido. http://github.com/crodas/Haanga/blob/master/lib/Haanga/Compiler/Parser.y
Si te fijas hay varias reglas definidas ahí, como ser if, for, else, buffer, T_CUSTOM_TAG, T_CUSTOM_BLOCK, T_SPACEFULL. En Django, hasta donde ví, todo es un tag, eso es muy maleable ya que pueden jugar bastante con sus tags (tienen hasta ‘if’ … ‘else’ definidos como ‘tags’). Personalmente prefiero hacer cosas así, en vez de hacerlo a cada petición, por eso es que tengo varias funciones definidar en lib/Haanga/Compiler.php y las “extensiones” en otro lugar.
Saludos
Me he centrado en testear únicamente el sistema de plantillas tanto en Haanga como en Django y Tornado.
La verdad es que las comparaciones son muy difíciles de hacer, ya que dependerá de lo que consideremos comparable. Por ejemplo en el caso de Django como bien se indica en un comentario anterior tenemos mucha más flexibilidad de trabajo con las plantillas, pero a un precio de velocidad, ya que las plantillas no se compilan a código Python
En el caso de Tornado pasa algo similar, Haanga genera código PHP, verifica que esté o no generado, lo genera si es necesario y lo ejecuta. Tornado por su parte también compila a código py, pero por defecto no lo guarda, de modo que si ejecutamos repetidas veces el código se volverá a compilar. Para poder comparar tenemos que ejecutar sólo el paso de generación de la plantilla.
Para ello he puesto un bucle tanto en benchmark de Tornado como en el código de php para el ejemplo b2_1.tpl. Sería lo más parecido a tener una plantilla dinámica compilada en ambos casos.
Ejecutando el bucle 1 sola vez tenemos y con el PHP precompilado tenemos unos tiempos de 0,023s de media (en mi máquina) para Haanga y 0,07 para Tornado. Si consideramos únicamente una ejecución donde el código no está compilado en Haanga, los tiempos de éste son de 0.057 de media.
Si consideramos el caso de múltiples acceso, donde ya tenemos la plantilla compilada tanto en Haanga como en Tornado, y pasándole la estructura de 100 objetos a la plantilla los benchsmarks arrojan 4,275s de media para Haanga y 2,225 de media para Tornado.
Como conclusión personal: Haanga es un lenguaje de plantillas muy rápido, sería el primero que probaría para mis aplicaciones PHP en este momento, pero para poder compararlo tenemos que establecer muy bien los criterios de comparación:
1. Tenemos que comparar sólo la generación de plantillas y no todo el framework.
2. Debemos conocer qué hace cada sistema de plantillas para poder crear tests comparables.
En producción el framework que utilicemos también nos puede dar un extra de velocidad. En el caso de Tornado, guardar en memoria las plantillas compiladas no daría un plus de velocidad respecto a Haanga o Django, pero siempre habría que evaluar plantilla a plantilla y con un criterio importante de mantenibilidad siempre presente.