LLVM: las partes problemáticas
(npopov.com)- Analiza desde múltiples ángulos las limitaciones estructurales y la deuda técnica del proyecto LLVM, y señala de forma concreta las áreas que necesitan mejoras
- Presenta cuellos de botella en la operación de un gran proyecto open source, como falta de revisión, inestabilidad de la API, tiempos de build y compilación, e inestabilidad del CI
- Entre los problemas de diseño del IR se incluyen el manejo de valores
undef, la codificación de restricciones, la semántica de punto flotante y la incompletitud de la especificación - Señala problemas estructurales de largo plazo, como la heterogeneidad de los backends, la confusión en el manejo del ABI y el retraso en la migración de GlobalISel y del pass manager
- Más que presentar el estado de LLVM de forma negativa, lo plantea como una oportunidad para la mejora continua y para ampliar las contribuciones
Problemas estructurales principales
-
La falta de capacidad de revisión se señala como el mayor cuello de botella
- Hay muchos autores de código, pero faltan revisores, por lo que a veces se fusionan cambios sin suficiente validación
- Como la estructura hace que pedir revisión sea responsabilidad del autor, a los nuevos contribuidores les resulta difícil encontrar revisores adecuados
- Se menciona como posible mejora la adopción del sistema de asignación automática de PRs de Rust
-
Los cambios frecuentes (churn) en la API y el IR cargan a los usuarios
- La API de C es relativamente estable, pero la API de C++ cambia con frecuencia, aumentando el costo de mantenimiento de frontends y backends
- La filosofía de “Upstream or GTFO” hace que el código no compartido no se refleje en la toma de decisiones
-
Problema de tiempos de build excesivos
- LLVM está compuesto por más de 2.5 millones de líneas de código C++, por lo que el build tarda mucho; en builds de depuración también se disparan el uso de memoria y disco
- Se discuten como mejoras posibles los headers precompilados (PCH), el build por defecto como
dyliby la daemonización de pruebas
-
Inestabilidad del CI
- Más de 200 buildbots prueban en entornos diversos, pero no se logra mantener siempre un “estado verde”
- Las pruebas flaky y los problemas de los buildbots diluyen las señales de alerta, dificultando detectar errores reales
- La introducción de pruebas previas para PRs ha mejorado algo la situación, pero la solución de fondo sigue siendo insuficiente
-
Falta de pruebas end-to-end
- Las pruebas unitarias de optimizaciones individuales son sólidas, pero casi no hay pruebas del pipeline completo ni de integración con backends
- Existe
llvm-test-suite, pero no cubre suficientemente combinaciones básicas de operaciones y tipos de datos
Problemas relacionados con backend y rendimiento
-
Heterogeneidad entre backends
- Aunque las etapas intermedias están unificadas, en los backends hay muchas modificaciones independientes por objetivo, lo que aumenta la duplicación y la divergencia
- Existe la tendencia a agregar hooks específicos por objetivo en vez de optimizaciones comunes
-
Tiempo de compilación
- Es lento en JIT y en lenguajes que generan IR a gran escala, como Rust y C++
- Los builds con
-O0son especialmente lentos, y el backend TPDE se presenta como una alternativa hasta 10~20 veces más rápida
-
Ausencia de seguimiento de rendimiento
- No existe una infraestructura oficial para rastrear el rendimiento en tiempo de ejecución
- El sistema LNT tiene problemas de inestabilidad operativa, UX y falta de datos, por lo que su efectividad es baja
Problemas de diseño del IR
-
Complejidad en el manejo de valores
undef- Pueden tener un valor distinto en cada uso, lo que provoca errores durante las optimizaciones
- En el futuro podrían reemplazarse por valores
poison, pero el manejo depoisonen memoria aún está incompleto
-
Incompletitud e inconsistencias de la especificación
- Existen casos antiguos de mal funcionamiento que siguen sin resolverse
- Persisten retos de diseño como el modelo de provenance
- Para abordar esto se formó un grupo de trabajo de especificación formal
-
Falta de consistencia en la codificación de restricciones
- Se mezclan distintos métodos, como flags
poison, metadata, atributos yassumes - La pérdida de información o su conservación excesiva afecta negativamente a las optimizaciones
- Se mezclan distintos métodos, como flags
-
Problemas en la semántica de punto flotante (FP)
- Se producen inconsistencias con NaN con señal, entornos no estándar, manejo de denormals y precisión extendida de x87
- Al tratarse aparte mediante intrinsics FP restringidos, aumenta la complejidad
Otros problemas técnicos
-
Retrasos en migraciones parciales
- El nuevo pass manager solo se aplica hasta las etapas intermedias; los backends siguen usando el anterior
- GlobalISel no ha logrado una transición completa en 10 años y coexiste con SDAG
-
Confusión en el manejo del ABI y las calling conventions
- La separación de responsabilidades entre frontend y backend no está clara y hay poca documentación
- Está en marcha la introducción de una biblioteca ABI y su implementación prototipo
- También existe el problema de que el ABI cambie según se activen ciertas funciones del objetivo
-
Inconsistencia en la gestión de builtins y libcalls
- TargetLibraryInfo y RuntimeLibcalls están separados, lo que reduce la consistencia
- No se puede reconocer la disponibilidad según el tipo de runtime library (
libgcc,compiler-rt, etc.) - Falta un punto de personalización para runtimes externos como Rust
-
Ineficiencia de la estructura Context / Module
- Los tipos y constantes viven en Context, mientras que las funciones y globales están en Module
- La falta de acceso al data layout genera incomodidades en tareas como el constant folding
- No se pueden enlazar contextos cruzados, por lo que hace falta simplificar la estructura
-
Presión de registros causada por LICM (movimiento de código invariante de bucle)
- Se hace hoist sin un modelo de costos
- Como el backend no vuelve a hacer sink, aumentan los spills y reloads
Conclusión
- Los problemas enumerados son retos estructurales derivados de la madurez y escala de LLVM, y se presentan como una oportunidad para mejorar la calidad del proyecto y la experiencia de los contribuidores
- En algunas áreas, como la optimización del build, la biblioteca ABI y el seguimiento de rendimiento, ya hay trabajos de mejora en curso
- En conjunto, LLVM sigue siendo poderoso, pero el refactoring continuo y el ajuste de la especificación son indispensables
1 comentarios
Comentarios en Hacker News
El texto en general está bien organizado, así que coincido bastante.
Últimamente la estabilidad de LLVM IR ha mejorado bastante. Rebasé Fil-C de LLVM 17 a 20 en un solo día.
En otros proyectos también mantuve el mismo pass en varias versiones de LLVM y no hubo mayores problemas.
Aun así, la presión de registros en LICM es especialmente grave en fuentes que no son C/C++. El problema parece menos de LICM en sí y más de lograr que regalloc aprenda mejor a rematerialize
Estaría bien abrir más opciones para que quienes desarrollan frontends puedan hacer benchmarks con distintas configuraciones y elegir el valor óptimo
Como cada herramienta y componente tiene sus propias reglas, hasta parece natural que haya diferencias entre versiones. Me pregunto si lo habré entendido mal
Cuando intenté compilar LLVM 18 en macOS, le pedí a la persona a cargo de compiler-rt que cambiara un solo boolean, pero el issue se cerró por estar “heated” y sigue sin resolverse desde hace 4 años.
Aun así, sigo queriendo a LLVM. clang-tidy, ASAN, UBSAN, LSAN, MSAN, TSAN son realmente excelentes.
Creo que escribir código C/C++ sin usar clang-tidy es una mala decisión.
Pero -fbounds-safety solo está en AppleClang, y MSAN/LSAN solo están en LLVM Clang. Xcode tampoco trae clang-tidy, clang-format ni llvm-symbolizer.
Al final, en macOS terminé teniendo que compilar Darwin LLVM por mi cuenta para usarlo.
Del lado de Linux también hay confusión. RHEL no trae libcxx, pero Fedora sí. Sin embargo, ninguna distribución trae libcxx instrumentada para MSAN.
Fedora casi llega, pero todavía hay que compilar compiler-rt manualmente
Después de pasar por discusiones recientes sobre LLVM, sentí que hace mucha falta una suite de pruebas ejecutables que parta de LLVM IR, no de C.
Si uno construye un backend directamente, falta documentación sobre SelectionDAG o GlobalISel, y el significado de varias operaciones no es claro, así que es fácil implementar mal las cosas
La API de C se siente como algo dejado de lado dentro de LLVM. Muchas opciones o passes de opt no están expuestos.
Como la mayoría de quienes desarrollan usan directamente la API de C++, la API de C queda relegada y termina siendo una ciudadana de segunda
Como el code review no se traduce en una recompensa inmediata, la gente tiende a no hacerlo mucho.
Si también se diera crédito de contribución por revisar, probablemente habría más motivación.
Hace 6 años compilaba LLVM seguido en una laptop Dell 9360 con 8 GB de RAM. Reduciendo el paralelismo del enlazado se podía hacer dentro del límite de memoria.
Me pregunto si hoy todavía se puede compilar con 8 GB.
En los primeros días de LLVM, una de sus ventajas era la velocidad de compilación más rápida que GCC.
Ahora, 23 años después de LLVM, me pregunto si aparecerá algo nuevo otra vez.
También hay alternativas como Cranelift, que no usa LLVM IR (Cranelift GitHub)
El manejo del ABI y las convenciones de llamada es el mayor dolor.
En el frontend del compilador hay que gestionar directamente el paso de argumentos y, a veces, incluso calcular la cantidad de registros
En el texto se decía que “los frontends están protegidos gracias a una API de C estable”, pero en la práctica no es así.
Algunas APIs sí son estables, pero partes como Orc cambian con frecuencia.
Parece que LLVM casi no tiene un sistema de revisión de issues. Los reportes de bugs que envié también llevan años sin ser atendidos.