Clojure y Java: Solucionan Retraso de Inicio en OpenLDK

Fuentes: Bending the CLOS MOP for Java-Style Single Dispatch

Este artículo aborda un problema de rendimiento significativo encontrado al integrar Clojure (un lenguaje funcional) con OpenLDK, una implementación de Common Lisp que se ejecuta sobre la Máquina Virtual de Java (JVM). Inicialmente, el tiempo de inicio de Clojure en OpenLDK era excesivamente largo (casi tres horas), debido a un cuello de botella en la función invoke-special, que se utiliza para llamar a los constructores de clases en Java. El problema radica en la diferencia fundamental en cómo CLOS (Common Lisp Object System) y Java manejan el despacho de métodos. CLOS utiliza el despacho multi-método, considerando los tipos de todos los argumentos para seleccionar el método apropiado, mientras que Java utiliza el despacho de un solo método, donde solo el tipo del objeto receptor importa. OpenLDK, al mapear clases Java a clases CLOS y métodos virtuales Java a funciones genéricas CLOS, incurre en el costo completo del protocolo de despacho multi-método de CLOS para cada llamada a un método Java, lo que se vuelve catastrófico con los aproximadamente 3000 clases que Clojure carga al inicio.

La solución implementada consiste en modificar el Meta-Object Protocol (MOP) de CLOS para reemplazar el mecanismo de despacho estándar con uno que se alinee con el modelo de despacho de un solo método de Java. Esto se logra mediante la creación de una nueva metaclas llamada java-generic-function, que introduce dos tablas hash en caché (dispatch-cache e invoke-special-cache) y un bloqueo para manejar la concurrencia. La dispatch-cache almacena funciones de método efectivas pre-calculadas, mientras que invoke-special-cache hace lo mismo para las llamadas invokespecial (utilizadas para llamar a métodos de la clase padre). La función compute-discriminating-function se especializa para java-generic-function para realizar búsquedas en la tabla hash en lugar de recorrer la cadena de despacho completa de CLOS. Un aspecto clave es que la lista de clases se especializa solo en la clase del receptor, ignorando los tipos de los demás argumentos, imitando así el despacho de un solo método de Java.

Además de la optimización del despacho, se implementó una solución para evitar la reconstrucción costosa de la red de discriminación (update-dfun) que CLOS realiza cada vez que se agrega o elimina un método o se define una nueva clase. En lugar de reconstruir la red, se borran las cachés y se reinstala la función de discriminación personalizada. Esto reduce significativamente el tiempo de inicio de Clojure y mejora el rendimiento general de OpenLDK. En resumen, la solución combina un despacho optimizado y una estrategia para evitar la reconstrucción innecesaria de estructuras internas, logrando una mejora sustancial en el rendimiento.