Cómo JIT integra nuestro código C # (heurística)

Inlining es una de las optimizaciones más importantes en compiladores. No solo elimina la sobrecarga de la llamada, sino que también abre muchas posibilidades para otras optimizaciones, por ejemplo, plegado constante, eliminación de código muerto, etc. Además, a veces, la alineación conduce a una disminución en el tamaño de la función de llamada. Le pregunté a algunas personas si sabían por qué reglas las funciones en C # están en línea, y la mayoría respondió que el JIT analiza el tamaño del código IL y solo incluye pequeñas funciones en el tamaño, digamos, hasta 32 bytes. Por lo tanto, decidí escribir esta publicación para revelar detalles de implementación con la ayuda de un ejemplo de este tipo, que mostrará varias heurísticas en acción a la vez:




¿Crees que la llamada al constructor de Volumen estará en línea aquí? Obviamente no. Es demasiado grande, especialmente debido a los nuevos operadores de lanzamiento de peso pesado , que conducen a un codegen bastante audaz. Veamos Disasmo:



¡en línea! ¡Además, todas las excepciones y sus ramas se han eliminado con éxito! Puede decir algo al estilo de "Ah, está bien, el jit es muy inteligente e hizo un análisis completo de todos los candidatos para inline, miró lo que sucedería si pasa argumentos específicos" o "El jit intenta alinear todo lo que es posible, realiza todas las optimizaciones y luego decide de manera rentable it or not ”(tome la combinatoria y calcule la complejidad de esta operación, por ejemplo, para un gráfico de llamadas de diez o dos métodos).

Bueno ... no, esto no es realista, especialmente en términos de justo a tiempo. Por lo tanto, la mayoría de los compiladores usan las llamadas observaciones y heurísticas para resolver este problema clásico de la mochila e intentan determinar su propio presupuesto y encajarlo de la manera más eficiente posible (y no, PGO no es una panacea). RyuJIT tiene observaciones positivas y negativas. Incremento positivo del coeficiente de beneficio (multiplicador de beneficio). Cuanto mayor sea el coeficiente, más código podemos incluir. Por el contrario, observaciones negativas: reduzca o incluso prohíba la inserción. Veamos qué observaciones hizo RyuJIT para nuestro ejemplo:



estas observaciones se pueden ver en los registros de COMPlus_JitDump (por ejemplo, en Disasmo):



todas estas observaciones simples aumentaron el coeficiente de 1.0a 11,5 y ayudado a superar con éxito el presupuesto de la inlineer, por ejemplo, el hecho de que se pasa un argumento constante y se compara con otra constante nos dice que con un alto grado de probabilidad después de colapsar las constantes se borrará una de las ramas de condición y el código será más pequeño. O, por ejemplo, el hecho de que este es un constructor y se llama dentro del bucle también es una pista al jit de que debería suavizar los requisitos para la inserción.

Además del multiplicador de beneficios, RyuJIT también usa observaciones para predecir el tamaño del código de función nativa y su impacto en el rendimiento usando constantes mágicas en EstimateCodeSize () y EstimatePerformanceImpact () obtenidas usando ML.

Por cierto, ¿notaste este truco?

if ((value - 'A') > ('Z' - 'A'))

Esta es una versión optimizada para:

if (value < 'A' || value > 'Z')

Ambas expresiones son una y la misma, pero en el primer caso tenemos una unidad base, y en el segundo hay tres de ellas. Resulta que el inliner tiene un límite estricto en el número de bloques base en la función y si excede de 5, entonces no importa cuán grande sea nuestro multiplicador de beneficios: la inline se cancela. Así que apliqué este truco para encajar en este requisito estricto. Sería genial si Roslyn lo hiciera por mí.

Número en Roslyn : github.com/dotnet/runtime/issues/13347
of PR en RyuJIT (mi incómodo intento): github.com/dotnet/coreclr/pull/27480

Allí describí un ejemplo de por qué tiene sentido hacerlo no solo en Jit sino y en el compilador de C #.

Métodos virtuales y en línea


Aquí todo está claro, no se puede indicar sobre qué no hay información en la etapa de compilación, aunque si el tipo o método está sellado, ¿por qué no ?

Excepciones en línea y de lanzamiento


Si un método nunca devuelve un valor (por ejemplo, simplemente lo hace throw new...), entonces dichos métodos se marcan automáticamente como ayudantes de lanzamiento y no estarán en línea. Esta es una forma de barrer el complejo codegen de throw newdebajo de la alfombra y apaciguar el interior.

Inlineado y atributo [AggressiveInlining]


En este caso, recomienda que el inlineer incorpore el método, pero debe ser extremadamente cuidadoso por dos razones:

  • Quizás optimice un caso y empeore todos los demás (por ejemplo, mejore el caso de argumentos constantes) por el tamaño del codegen.
  • La alineación a menudo genera una gran cantidad de variables temporales que pueden superar un cierto límite: la cantidad de variables cuyo ciclo de vida puede rastrear RyuJIT (512) y después de eso, el código comenzará a convertirse en terribles derrames en la pila y disminuirá considerablemente. Dos buenos ejemplos: tyts y tyts .

Métodos dinámicos y en línea.


Actualmente, dichos métodos no están en línea y no están en línea: github.com/dotnet/runtime/issues/34500

Mi intento de escribir mi heurística


Recientemente, traté de escribir su propia heurística para ayudar aquí en tal ocasión:



en una publicación anterior mencioné que recientemente optimicé el cálculo de RyuJIT de la longitud de las cadenas constantes ( "Hello".Length -> 5vemos que si zainlaynit), y así, en el ejemplo anterior ^ Validateen Test, obtenemos if ("hello".Length > 10)lo que está optimizado en if (5 > 10)lo que está optimizado en la eliminación de toda la condición / rama. Sin embargo, el inliner se negó a alinearse Validate:



y el principal problema aquí es que aún no existe una heurística que le diga al jit que estamos pasando una cadena constante System.String::get_Length, lo que significa que la llamada callvirt probablemente colapsará en una constante y se eliminará toda la rama. En realidad, mi heurísticay agrega esta observación (el único inconveniente es que tiene que resolver todos los callvirts, que no es muy rápido).

Hay otras restricciones, una lista de las cuales se puede encontrar en general aquí . Y aquí puede leer los pensamientos de uno de los principales desarrolladores de JIT sobre el diseño del inliner y su artículo sobre el uso de Machine Learning para este caso.

All Articles