Ahora, el tema del aprendizaje automático y la inteligencia artificial es extremadamente popular, por el momento, gracias a la potencia informática de las computadoras, las ideas y los algoritmos que han surgido durante mucho tiempo se pueden implementar y mejorar significativamente. Casi todos los días puedes leer noticias sobre nuevos logros en esta área. Además, el aprendizaje automático se utiliza en casi todas las áreas ... y el desarrollo de compiladores no es una excepción. Sin embargo, el área es bastante específica y tiene sus propias características y dificultades para crear compiladores inteligentes. Al mismo tiempo, hay muchos estudios sobre este tema y se han llevado a cabo durante mucho tiempo tanto en el entorno académico como en diversas empresas.
¿Dónde exactamente están tratando de aplicar métodos de aprendizaje automático al crear compiladores? ¿Y por qué hasta ahora los compiladores "inteligentes" no se han convertido en parte de la vida diaria del desarrollador?
Opciones para usar el aprendizaje automático en el desarrollo del compilador
Comencemos con la primera pregunta sobre usos específicos del aprendizaje automático. El hecho es que los compiladores modernos son sistemas complejos con una gran cantidad de optimizaciones que le permiten obtener un código de máquina más eficiente. Sin embargo, algunas de las optimizaciones y otras tareas, como la asignación de registros, son NP completas, lo que obliga a los desarrolladores de compiladores a usar algoritmos heurísticos. Como resultado, la mayoría de los compiladores tienen una gran cantidad de indicadores de optimización que le permiten configurar la heurística utilizada. En LLVM, casi todos los pasajes tienen varias opciones ocultas que pueden afectar su funcionamiento, se pueden usar con el indicador –mllvm al llamar a clang o en la utilidad opt. Sin embargo, esta variedad de indicadores está oculta detrás de las opciones que se usan con mucha más frecuencia, que contienen muchas configuraciones a la vez y generalmente se denominan niveles de optimización. Para los compiladores C / C ++, estos son conocidos por la mayoría de -O1, -O2, -O3 para optimizar el tiempo de ejecución y -Os para optimizar el tamaño del código. Pero, desafortunadamente, el código óptimo no siempre es el resultado (los expertos en ensambladores pueden reescribir el código generado de la mejor manera), mucho depende del código fuente en un lenguaje de alto nivel, arquitectura de procesador, características de lenguaje, etc.
A pesar de que hoy en día los procesadores modernos tienen suficiente RAM y un rendimiento bastante alto, todavía hay áreas donde el rendimiento de la aplicación, la eficiencia energética y el tamaño del código de la máquina juegan un papel clave. Ejemplos de tales áreas incluyen el desarrollo de software para sistemas embebidos con una cantidad limitada de RAM, procesamiento de señal digital, sistemas en tiempo real, etc. Por lo tanto, en los casos en que necesite obtener un código de máquina de alto rendimiento para sistemas lo suficientemente grandes, la selección de las opciones de compilación correctas que brinden el mejor resultado es una tarea importante. Además, el problema del peor tiempo de
ejecución (
WCET ) no
ha desaparecido cuando los sistemas en tiempo real necesitan calcular y minimizar, si es posible, el tiempo de ejecución de una tarea específica en la plataforma. Hasta ahora, los programadores que trabajan con sistemas con una cantidad limitada de RAM no pueden confiar completamente en los compiladores y, a menudo, optimizan independientemente el código de máquina generado.
Es difícil para una persona predecir qué optimizaciones darán un buen resultado y cuáles pueden dar lugar a regresiones, porque para esto necesita comprender bien las complejidades de los algoritmos heurísticos utilizados, un buen conocimiento de la estructura y los pasos del compilador utilizado, y también conocer completamente el código del programa compilado, que El proceso de desarrollo de aplicaciones actual es imposible. Como resultado, identificar las mejores opciones de compilación para un programa para una persona se convierte en una tarea de búsqueda exhaustiva de varias combinaciones de opciones y medidas de rendimiento y tamaños de código.
Además, existe una limitación en la forma de una unidad de compilación con la que puede trabajar y para la que puede elegir opciones. Entonces, para C / C ++, este sigue siendo un archivo que puede contener una gran cantidad de código, lo que, quizás, sería útil para optimizar de diferentes maneras, pero por el momento esto no es posible. Por lo tanto, un compilador "inteligente" que podría entrenar y luego obtener un código bien optimizado para una variedad de casos es un sueño para algunos desarrolladores.
Investigación y soluciones existentes
Naturalmente, el problema de la selección automática de opciones de compilación ha sido de interés para los investigadores durante muchos años. Uno de los proyectos más famosos es el desarrollo de G. Fursin e investigadores de su equipo
MILEPOST GCC , que es una versión del compilador gcc que puede seleccionar pases de optimización basados en la capacitación previa en la muestra de datos obtenida. En este trabajo, utilizamos un conjunto de 55 características para resolver el problema y un modelo bastante simple basado en la idea de distribuir buenas soluciones basadas en el algoritmo K de los vecinos más cercanos. Fue este desarrollo el que mostró que los pases de optimización de ajuste pueden conducir a un código que es dos veces más rápido que el código obtenido usando la opción de optimización máxima estándar -O3.
También hay estudios de G. Pekhimenko y A.D. Brown para IBM TPO (
Toronto Portable Optimizer ). Su tarea principal era seleccionar valores seleccionables heurísticamente para optimizaciones y el conjunto de transformaciones de código. Para la implementación, se utilizó la regresión logística, lo que hizo posible realizar ajustes de multas efectivos para un entrenamiento más rápido. El clasificador fue construido en Matlab. La probabilidad de uso se calculó para cada pase, y se usó si era más del 50%. Como resultado de la característica que intentaron reducir en este estudio, fue el tiempo de compilación estático.
A.Askhari participó en la selección directa de opciones de compilación para todo el programa para minimizar el tiempo de ejecución, el tiempo de compilación, el tamaño del código y el consumo de energía. Para esto, se
utilizaron el
cTuning Framework y el
Collective Mind Framework desarrollados por G. Fursin y A. Lokhmotov (también desarrollado en
GitHub ).
También hay estudios de
M. Stephenson y S. Amarasinge de seleccionar optimizaciones para ciertos algoritmos especialmente importantes (asignación de registros, PREFETCHING DE DATOS, FORMACIÓN DE HIPERBLOQUE). Para cada función, sus propias características se utilizaron en consecuencia. Para la solución, se utilizó un algoritmo genético. Las pruebas del producto desarrollado se llevaron a cabo en el Open Research Compiler (ORC).
También hay un proyecto
MAGEEC (compilador eficiente de energía guiado por máquina), cuyos objetivos son algo diferentes. La infraestructura desarrollada utiliza el aprendizaje automático para seleccionar las optimizaciones necesarias para generar el código con la máxima eficiencia energética para sistemas informáticos de alto rendimiento. MAGEEC está diseñado para funcionar con gcc y LLVM. Este compilador es parte del proyecto más grande TSERO (Total Software Energy Reporting and Optimization).
Una investigación directamente relacionada con LLVM es
LLVMTuner , un producto de software desarrollado en la Universidad de Illinois por I. Chen y W. Adwe. En 2017, se presentó un informe que describe los resultados disponibles en ese momento. En este trabajo, optimizamos ciclos individuales "calientes". Este marco está diseñado para la configuración automatizada de grandes programas. LLVMTuner se ejecuta en el middleware LLVM IR, utiliza perfiles para identificar bucles activos y luego ajusta automáticamente la heurística para ellos. La atención se centra en los ciclos de nivel superior. Los ciclos seleccionados y cualquier función de llamada se transfieren a un módulo separado, que está sujeto a las optimizaciones necesarias. Esta solución le permite obtener un rendimiento mejorado en programas grandes.
Problemas existentes
Sin embargo, no existe un compilador ampliamente utilizado que ajuste independientemente la heurística de la optimización de pasadas. Cual es el problema Como sabe, la efectividad de los métodos de aprendizaje automático y la calidad de los modelos obtenidos dependen de la elección correcta de las características y la calidad de los datos para el entrenamiento (a pesar de la existencia de algoritmos menos sensibles a los datos "ruidosos"). Sin conocer la estructura y los algoritmos utilizados en el compilador, no es fácil seleccionar un conjunto completo y suficiente de características para el entrenamiento, aunque existen algunas bastante claras y lógicas, por ejemplo, el tamaño del ciclo, el número de salidas del ciclo, etc. Por lo tanto, es difícil desarrollar una solución universal adecuada para muchos compiladores a la vez, y no es un hecho que generalmente sea posible. Además, es probable que esto no sea necesario.
Dado que el desarrollo de compiladores debe ser eficiente y factible en un tiempo bastante corto, es natural que incluso las grandes empresas desarrollen sus compiladores industriales basados en soluciones preparadas. La mayoría de las soluciones modernas se pueden dividir en dos categorías: ejecutando en máquinas virtuales, por ejemplo, JVM - compiladores JIT, y compiladores basados en LLVM, un sistema que implementa una máquina virtual con instrucciones similares a RISC - compiladores estáticos y dinámicos. Por supuesto, todavía existen soluciones propias de las empresas, pero se están volviendo menos competitivas debido a la falta de una gran comunidad involucrada en el desarrollo de las tecnologías utilizadas en ellas. Como resultado, hoy en día muchas grandes empresas como Google, Apple, Adobe, ARM utilizan LLVM para desarrollar sus propias soluciones. Por supuesto, gcc sigue siendo el compilador principal para C / C ++, existen otras soluciones para otros lenguajes, pero de todos modos, si, por ejemplo, se encuentra una solución para LLVM, esto ya cubrirá una parte decente de los compiladores existentes actualmente.
La recopilación de características para el entrenamiento también se convierte en un gran problema, ya que los compiladores de múltiples pasos transforman fuertemente la representación intermedia, y las características recopiladas en la etapa inicial no son muy relevantes para las optimizaciones posteriores del compilador, estas características pueden cambiar con una alta probabilidad. Además, tiene sentido recopilar por separado para diferentes tipos de elementos: módulos, ciclos, bloques base, ya que las optimizaciones generalmente están diseñadas para cambiar un tipo particular de elemento, en LLVM, incluso de acuerdo con este criterio, los pasajes se dividen.
Pero, en primer lugar, surge la cuestión de identificar los elementos para los cuales es necesario recopilar características. Hay muchas formas de calcular identificadores únicos que se pueden guardar durante todas las optimizaciones, por ejemplo:
- Hash de front end basado en AST
- números únicos asignados en el análisis frontal
- Número de 64 bits generado sobre la base de arcos en CFG (gráfico de flujo de control) utilizando una suma de verificación (similar a PGO (Optimización guiada por perfil) en LLVM)
Sin embargo, debe guardar correctamente estos identificadores durante las transformaciones, cuando los elementos pueden fusionarse en uno, dividirse, crear nuevos y eliminar los originales, lo cual no es una tarea fácil.
En segundo lugar, es difícil, en principio, evaluar los límites de los ciclos fuente, los bloques base, etc., escritos en el código fuente, en el IR ya convertido. Por ejemplo, debido a la generación de múltiples etapas del código de máquina adoptado por LLVM, la información sobre las unidades base de la máquina se pierde después de la generación del código según las instrucciones de la máquina en AsmPrinter. Y, en consecuencia, también se pierde información sobre los identificadores de los bloques y ciclos base, para lo cual, por ejemplo, se mide el desplazamiento desde el comienzo de la función, por lo tanto, con este método, solo en la etapa de generación del código de máquina se puede obtener el desplazamiento en forma de número de bytes. Sin embargo, en las etapas posteriores de generación de código de máquina cuando se trabaja con fragmentos de máquina, se pueden agregar varias alineaciones, que cambian el tamaño de las instrucciones tomadas en cuenta anteriormente, y también se agregan instrucciones nop. Debido a esto, para los bloques base al final de las funciones grandes, el error de cálculo puede ser muy grande, hasta un cambio completo a otro bloque / ciclo. Y aunque algunas de las transformaciones en las etapas posteriores se pueden rastrear y tener en cuenta, esto no dará garantías para la precisión de las mediciones, ya que el tamaño de las instrucciones puede variar hasta el enlazador.

Como puede ver, incluso la colección de atributos sobre la base de la cual se necesita capacitación es bastante complicada y requiere mucho tiempo, y que en el futuro probablemente se convierta en el conjunto de entrada para el modelo capacitado para la toma de decisiones. Y no hay soluciones obvias para estos problemas, lo que complica el trabajo inmediato asociado con el aprendizaje automático y atrae a un gran número de personas debido a la falta de suficientes conjuntos de datos. Bueno, las dificultades típicas de encontrar soluciones a los problemas de aprendizaje automático, elegir modelos, métodos, determinar el subconjunto correcto de atributos con una gran cantidad de ellos, etc. existe en este caso. Casi todos los que se han encontrado con el aprendizaje automático saben sobre ellos y, tal vez, algo único y específico para los compiladores no está aquí.
Es difícil predecir cuándo los compiladores inteligentes se generalizarán. Los compiladores modernos también tienen otros problemas que es poco probable que se resuelvan con este método, y que en este momento probablemente sean más prioritarios. Sin embargo, los compiladores ya se han vuelto mucho más inteligentes de lo que eran al comienzo de su aparición, y este proceso continuará, aunque puede ser algo más lento de lo que a la mayoría de los desarrolladores les gustaría.