El scripting visual va lento
Publicado el 27 de mayo de 2020
El scripting visual es importante. Si alguien aún sigue pensando que esto de programar con cajitas es para niños, sólo tiene que mirar al hecho de que tanto Unreal como Unity tienen sistemas de scripting visual, y es conocido que muchos motores privados incorporan estas técnicas también. El scripting visual es importante porque conforme más de nuestras herramientas se vuelven programables, más gente que no necesariamente tiene un trasfondo en ingeniaría del software tiene que ser capaz de programar para poder explotar a fondo estas herramientas. Que artistas y diseñadores sean capaces de programar puede reducir mucho los tiempos de iteración y por lo tanto acelerar mucho la velocidad de producción de un proyecto. Sin embargo, la forma tradicional de programar, el texto, puede ser innecesariamente difícil o incluso impenetrable para algunas de estas personas, que sin embargo al tener una mentalidad muy visual pueden entender esto de programar con cajitas mucho más fácilmente.
Blueprints de Unreal
Sin embargo, al hablar sobre el scripting visual (sobre todo con programadores), siempre surge el temita: va lento. Parece que, como si una antigua maldición vudú pesara sobre él, el código en cajitas está irremediablemente condenado a ser más lento que su contraparte textual. Este es el principal motivo por el que algunos programadores se oponen a esta herramienta o por la que proyectos se ven lastrados por esos valiosos milisegundos perdidos en nada. Lo que más me choca es que nadie parece preguntarse por qué es el scripting visual más lento, ni si tiene que ser necesariamente así. Es más fácil quedarse a un lado y culpar a los hijos de puta como en este famoso gag de
South Park
.
Lo cierto es que la implementación de la mayoría de sistemas de scripting visual es más bien pobre, sobre todo comparada con todos los años de investigación que se han invertido en los compiladores modernos, capaces de optimizar hasta el punto de que escribir código de ensamblador a mano hoy en día es directamente contraproducente. El compilador lo va a hacer mejor que tú. En cambio, los sistemas de scripting visual han sido escritos principalmente por programadores de videojuegos, que no tienen por qué ser expertos en lenguajes de programación, y que han recurrido a la implementación más directa e intuitiva, que no es en absoluto la más rápida. Por lo general nos vamos a encontrar con una clase abstracta de Nodo, con una función virtual de ejecutar, y vectores de conexiones a otros nodos. Los parámetros y el resultado de la función de ejecutar tienen que ser también polimórficos para poder permitir cualquier tipo de función.
C++
// Versión muy simplificada del código
struct Node
{
	virtual any run(vector<any> const & params) = 0;

	vector<Node *> input_connections;
	Node * output_connection;
};
La estructura de datos sería entonces un grafo dirigido de nodos polimórficos y por lo tanto en bloques de memoria separados. Todos los parámetros que viajan por este grafo van también encapsulados en un envoltorio que los hace polimórficos. Atravesar este grafo requiere de una enorme cantidad de saltos en memoria que dilapidan el uso de la memoria caché y de comprobaciones innecesarias para asegurar la corrección de tipos durante la ejecución. Comprobaciones que podrían hacerse una sola vez de antemano y luego confiar en que estarán bien. Esta es la implementación más evidente. Al pensar en las cajitas conectadas por líneas que el usuario va a manipular en su editor, es muy fácil pensar en que la estructura en memoria debería ser análoga y por lo tanto lo más obvio es pensar en un grafo. Sin embargo, esta estructura no es en absoluto la más eficiente y hace falta dar un paso adicional de abstracción.
Comparemos esto con la estructura de datos del código del lenguaje de máquina. Una función es una secuencia contigua de instrucciones, lo que permite que al cargar una instrucción se carguen también las instrucciones que muy probablemente se ejecutarán después de esa. El hecho de que las instrucciones estén ubicadas en memoria en el orden en el que se van a ejecutar elimina la necesidad de guardar las conexiones, reduciendo la cantidad de memoria y sobre todo de saltos en memoria requeridos, además de contribuir a aumentar la localidad de memoria, como hemos mencionado antes. Las variables viven en una pila también contigua en memoria a menos que el programador escriba lo contrario, de forma que la manipulación de variables locales y parámetros sucede siempre en caché, y sus tipos son conocidos y usados para optimizar el código, en vez de encapsulados en contenedores polimórficos que hacen que acceder a ellos sea más lento. Esto se debe al hecho de que el código se compila. Un programa en texto no es ejecutable. El texto es manipulado por el compilador para generar el código de máquina que sí es ejecutable. Además, el código se optimiza. La secuencia de instrucciones final generada por el compilador por lo general acaba pareciéndose más bien poco al código escrito por el programador. En vez de eso se le aplican toda serie de transformaciones para conseguir un programa que produzca unos resultados equivalentes con menos trabajo.
Y esta técnica no es en absoluto incompatible con los lenguajes de scripting visual, que son lenguajes formales igual que los representados en texto. Unreal intenta hacer algo parecido con poco éxito con su herramienta para “nativizar” sus blueprints, que consiste en generar código en C++ a partir del grafo y compilar eso. Sin embargo, si miramos el código que es generado por esta herramienta, vemos que ninguna persona en su sano juicio escribiría código así jamás, y que está plagada de ineficiencias por el hecho de que sigue pensando en grafos y nodos, aunque lo haga en código compilado y no interpretado. Lo que yo propongo es usar ese grafo para generar el árbol de sintaxis abstracta (abstract syntax tree, AST) del programa, que luego puede ser usado para generar código de máquina, optimizado por un optimizador, o incluso interpretado. Incluso se podría hacer ambas. Se podría tener un intérprete que actualice rápidamente los cambios hechos por el programador para poder iterar más fácilmente, y luego un compilador para que la versión final vaya más rápido. El hecho de que el concepto de AST o de optimizador sean desconocidos para los sistemas de scripting visual de los motores de videojuegos me hace pensar que hay mucho trabajo por hacer en una herramienta que es tan fundamental para el desarrollo.
Al final, en un asombroso giro edípico son los propios programadores, esos que se quejan de que el scripting visual va lento, los causantes de esa lentitud, con implementaciones muy ingenuas de un sistema tan crítico. Hay mucho trabajo por hacer en ese campo y ganancias potenciales muy grandes que me gustaría ver algún día.
Logo of RSS.