Runtime: Introducir un JIT escalonado

Creado en 14 abr. 2016  ·  63Comentarios  ·  Fuente: dotnet/runtime

¿Por qué .NET JIT no está en niveles?

El JIT tiene dos objetivos principales de diseño: un tiempo de inicio rápido y un alto rendimiento de estado estable.

Al principio, estos objetivos parecen contradecidos. Pero con un diseño JIT de dos niveles, ambos son alcanzables:

  1. Todo el código comienza interpretado. Esto da como resultado un tiempo de inicio extremadamente rápido (más rápido que RyuJIT). Ejemplo: El método Main es casi siempre frío y ponerlo a prueba es una pérdida de tiempo.
  2. El código que se ejecuta con frecuencia se elimina mediante un generador de código de muy alta calidad. Muy pocos métodos estarán calientes (¿1%?). Por lo tanto, el rendimiento del JIT de alta calidad no importa mucho. Puede pasar tanto tiempo como un compilador de C para generar un código muy bueno. Además, puede _asumir_ que el código está activo. Puede alinearse como un loco y desenrollar bucles. El tamaño del código no es una preocupación.

Llegar a esta arquitectura no parece demasiado costoso:

  1. Escribir un intérprete parece barato en comparación con un JIT.
  2. Se debe crear un generador de código de alta calidad. Esto podría ser VC, o el proyecto LLILC.
  3. Debe ser posible hacer la transición del código en ejecución interpretado al código compilado. Esto es posible; la JVM lo hace. Se llama reemplazo de pila (OSR).

¿El equipo JIT está siguiendo esta idea?

.NET se ejecuta en cientos de millones de servidores. Siento que queda mucho rendimiento sobre la mesa y se desperdician millones de servidores para los clientes debido a una generación de código subóptima.

categoría:rendimiento
tema: grandes apuestas
nivel de habilidad: experto
costo: extra grande

area-CodeGen-coreclr enhancement optimization tenet-performance

Comentario más útil

Con relaciones públicas recientes (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), también tiene la capacidad de especificar la compilación en niveles como una configuración de tiempo de ejecución. json o una propiedad de proyecto de msbuild. El uso de esta funcionalidad requerirá que esté en compilaciones muy recientes, mientras que la variable de entorno ha existido por un tiempo.

Todos 63 comentarios

@GSPP Tiering es un tema constante en las conversaciones de planificación. Mi impresión es que se trata de _cuándo_, no de _si_, si eso proporciona algún consuelo. En cuanto a _por qué_ no está ya allí, creo que se debe a que, históricamente, las ganancias potenciales percibidas no justificaron los recursos de desarrollo adicionales necesarios para administrar la mayor complejidad y el riesgo de múltiples modos de generación de código. Sin embargo, debería dejar que los expertos hablen de esto, así que los agregaré.

/cc @dotnet/jit-contrib @russellhadley

De alguna manera, dudo que esto siga siendo relevante en un mundo de crossgen/ngen, Ready to Run y ​​corert.

Ninguno de estos ofrece un alto rendimiento de estado estable en este momento, que es lo importante para la mayoría de las aplicaciones web. Si alguna vez lo hacen, estoy feliz con eso ya que personalmente no me importa el tiempo de inicio.

Pero hasta ahora todos los generadores de código para .NET han tratado de hacer un acto de equilibrio imposible entre los dos objetivos, sin lograr ninguno de los dos muy bien. Deshagámonos de ese acto de equilibrio para que podamos convertir las optimizaciones en 11.

Pero hasta ahora todos los generadores de código para .NET han tratado de hacer un acto de equilibrio imposible entre los dos objetivos, sin lograr ninguno de los dos muy bien. Deshagámonos de ese acto de equilibrio para que podamos convertir las optimizaciones en 11.

Estoy de acuerdo, pero arreglar esto no requiere cosas como un intérprete. Solo un buen compilador crossgen, ya sea un mejor RyuJIT o LLILC.

Creo que la mayor ventaja es para las aplicaciones que necesitan generar código en tiempo de ejecución. Estos incluyen lenguajes dinámicos y contenedores de servidor.

Es cierto que el código generado dinámicamente es una motivación, pero también es cierto que un compilador estático nunca tendrá acceso a toda la información disponible en tiempo de ejecución. No solo eso, incluso cuando especula (p. ej., basándose en información de perfil), es mucho más difícil para un compilador estático hacerlo en presencia de un comportamiento modal o dependiente del contexto externo.

Las aplicaciones web no deberían necesitar ningún procesamiento de estilo ngen. No encaja bien en la canalización de implementación. Se necesita mucho tiempo para generar binarios grandes (incluso si casi todo el código está dinámicamente inactivo o inactivo).

Además, al depurar y probar una aplicación web, no puede confiar en ngen para obtener un rendimiento realista.

Además, estoy de acuerdo con el punto de Carol de usar información dinámica. El nivel de interpretación puede perfilar el código (ramas, conteos de viajes en bucle, objetivos de despacho dinámico). ¡Es una combinación perfecta! Primero recopile el perfil, luego optimice.

Los niveles resuelven todo en todos los escenarios para siempre. Hablando aproximadamente :) Esto realmente puede llevarnos a la promesa de los JIT: lograr un rendimiento _más allá_ de lo que puede hacer un compilador de C.

La implementación actual de RyuJIT tal como es ahora es lo suficientemente buena para un Nivel 1... La pregunta es: ¿Tendría sentido tener un JIT de optimización extrema de Nivel 2 para rutas activas que puedan ejecutarse después del hecho? Esencialmente cuando detectamos o tenemos suficiente información de tiempo de ejecución para saber que algo está caliente o cuando se nos pide que lo usemos desde el principio.

RyuJIT es lo suficientemente bueno como para ser el nivel 1. El problema con eso es que un intérprete tendría un tiempo de inicio _mucho_ más rápido (en mi opinión). El segundo problema es que para avanzar al nivel 2, el estado local de ejecución del código de nivel 1 debe ser transferible al nuevo código de nivel 2 (OSR). Eso requiere cambios RyuJIT. Agregar un intérprete sería, creo, un camino más económico con una mejor latencia de inicio al mismo tiempo.

Una variante aún más económica sería no reemplazar el código en ejecución con el código de nivel 2. En su lugar, espere hasta que el código de nivel 1 regrese naturalmente. Esto puede ser un problema si el código entra en un bucle activo de larga duración. Nunca llegará al rendimiento de nivel 2 de esa manera.

Creo que eso no sería tan malo y podría usarse como una estrategia v1. Las ideas atenuantes están disponibles, como un atributo que marca un método como activo (esto debería existir de todos modos, incluso con la estrategia JIT actual).

@GSPP Eso es cierto, pero eso no significa que no lo sabrías en la próxima ejecución. Si el código y la instrumentación Jitted se vuelven persistentes, entonces en la segunda ejecución aún obtendrá el código de Nivel 2 (a expensas de un tiempo de inicio), que por una vez personalmente no me importa, ya que escribo principalmente código de servidor.

Escribir un intérprete parece barato en comparación con un JIT.

En lugar de escribir un nuevo intérprete, ¿podría tener sentido ejecutar RyuJIT con las optimizaciones deshabilitadas? ¿Eso mejoraría el tiempo de inicio lo suficiente?

Se debe crear un generador de código de alta calidad. Esto podría ser VC

¿Estás hablando de C2, el backend de Visual C++? Eso no es multiplataforma y no es de código abierto. Dudo que arreglar ambos suceda pronto.

Buena idea con la desactivación de optimizaciones. Sin embargo, el problema de OSR permanece. No estoy seguro de lo difícil que es generar código que permita que el tiempo de ejecución derive el estado arquitectónico de IL (locales y pila) en tiempo de ejecución en un punto seguro, cópielo en el código jitted de nivel 2 y reanude la función intermedia de ejecución de nivel 2. La JVM lo hace, pero quién sabe cuánto tiempo llevó implementarlo.

Sí, estaba hablando de C2. Creo recordar que al menos uno de los JIT de escritorio se basa en el código C2. Probablemente no funcione para CoreCLR pero tal vez para Desktop. Estoy seguro de que Microsoft está interesado en tener bases de código alineadas, por lo que probablemente eso esté descartado. LLVM parece ser una gran elección. Creo que actualmente varios idiomas están interesados ​​en hacer que LLVM funcione con GC y con tiempos de ejecución administrados en general.

LLVM parece ser una gran elección. Creo que actualmente varios idiomas están interesados ​​en hacer que LLVM funcione con GC y con tiempos de ejecución administrados en general.

Un artículo interesante sobre este tema: Apple recientemente movió el nivel final de su JavaScript JIT lejos de LLVM: https://webkit.org/blog/5852/introducing-the-b3-jit-compiler/ . Es probable que nos encontremos con problemas similares a los que ellos encontraron: tiempos de compilación lentos y la falta de conocimiento del idioma de origen por parte de LLVM.

10 veces más lento que RyuJIT sería totalmente aceptable para un segundo nivel.

No creo que la falta de conocimiento del idioma de origen (que es una verdadera preocupación) sea inherente a la arquitectura de LLVM. Creo que varios equipos están ocupados moviendo LLVM a un estado en el que el conocimiento del idioma de origen se puede utilizar más fácilmente. _Todos_ los lenguajes de alto nivel que no son C tienen este problema al compilar en LLVM.

El proyecto WebKIT FTL/B3 está en una posición más difícil para tener éxito que .NET porque deben sobresalir cuando ejecutan código que _en total_ consume unos pocos cientos de milisegundos de tiempo y luego se cierra. Esta es la naturaleza de las cargas de trabajo de JavaScript que controlan las páginas web. .NET no está en ese lugar.

@GSPP Estoy seguro de que probablemente conoce LLILC . Si no, echa un vistazo.

Hemos estado trabajando durante un tiempo en la compatibilidad con LLVM para los conceptos de CLR y hemos invertido en mejoras tanto de EH como de GC. Todavía queda un poco más por hacer en ambos. Más allá de eso, hay una cantidad desconocida de trabajo para que las optimizaciones funcionen correctamente en presencia de GC.

LLILC parece estar estancado. ¿Lo es?
El 18 de abril de 2016 a las 7:32 p. m., "Andy Ayers" [email protected] escribió:

@GSPP https://github.com/GSPP Estoy seguro de que probablemente conoce LLILC
https://github.com/dotnet/llilc. Si no, echa un vistazo.

Hemos estado trabajando durante un tiempo en la compatibilidad con LLVM para los conceptos de CLR y hemos
invirtió en mejoras de EH y GC. Todavía queda un poco más por hacer
ambos. Más allá de eso, hay una cantidad desconocida de trabajo para obtener optimizaciones
funcionando correctamente en presencia de GC.


Estás recibiendo esto porque comentaste.
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-211630483

@drbo : LLILC está en un segundo plano por el momento: el equipo de MS se ha centrado en lograr que aparezcan más objetivos en RyuJIT, así como en solucionar los problemas que surgen a medida que se lanzan las unidades CoreCLR y eso nos ha llevado prácticamente todo nuestro tiempo. Está en mi lista TODO (en mi abundante tiempo libre) escribir una publicación de lecciones aprendidas en función de lo lejos que hemos llegado (actualmente) con LLILC, pero aún no lo he hecho.
En cuanto a los niveles, este tema ha generado mucha discusión a lo largo de los años. Creo que dadas algunas de las nuevas cargas de trabajo, así como la nueva incorporación de imágenes versionables listas para ejecutar, analizaremos cómo y dónde colocar en niveles.

@russellhadley , ¿tuviste tiempo libre para escribir la publicación?

Tengo la hipótesis de que debería haber algo sobre las ranuras de pila no promocionadas y gcroots que rompen las optimizaciones y el tiempo de jitting lento ... Debería echar un vistazo al código del proyecto.

También me pregunto si es posible y rentable saltar directamente a SelectionDAG y realizar parte del backend de LLVM. Al menos alguna mirilla y propagación de copias... si, por ejemplo, la promoción de gcroot a los registros es compatible con LLILC

Tengo curiosidad sobre el estado de LLILC, incluidos los cuellos de botella actuales y cómo le va con RyuJIT. LLVM, al ser un compilador completo de "potencia industrial", debería tener una gran cantidad de optimizaciones disponibles para OSS. Ha habido algunas conversaciones sobre una serialización/deserialización más eficiente y rápida del formato de código de bits en la lista de correo; Me pregunto si esto es algo útil para LLILC.

¿Ha habido más pensamientos sobre esto? Se ha lanzado @russellhadley CoreCLR y RyuJIT se ha portado a (al menos) x86. ¿Qué sigue en la hoja de ruta?

Consulte dotnet/coreclr#10478 para conocer los comienzos del trabajo sobre esto.

También dotnet/coreclr#12193

@noahfalk , ¿podría proporcionar una forma de decirle al tiempo de ejecución que fuerce una compilación de nivel 2 de inmediato desde el código administrado? La compilación por niveles es una muy buena idea para la mayoría de los casos de uso, pero estoy trabajando en un proyecto en el que el tiempo de inicio es irrelevante pero el rendimiento y una latencia estable son esenciales.

Fuera de mi cabeza, esto podría ser:

  • una nueva configuración en el archivo de configuración, un cambio como <gcServer enabled="true" /> para obligar al JIT a omitir siempre el nivel 1
  • o algo así como RuntimeHelpers.PrepareMethod , que sería llamado por el código en todos los métodos que forman parte de la ruta activa (estamos usando esto para pre-JIT nuestro código en el inicio). Esto tiene la ventaja de dar un mayor grado de libertad al desarrollador que debe saber cuál es la ruta activa. Una sobrecarga adicional de este método estaría bien.

Por supuesto, pocos proyectos se beneficiarían de esto, pero estoy un poco preocupado por el hecho de que JIT se salta las optimizaciones de forma predeterminada, y no puedo decirle que prefiero que optimice mi código en gran medida .

Soy consciente de que escribiste lo siguiente en el documento de diseño:

Agregue una nueva etapa de canalización de compilación a la que se puede acceder desde las API de código administrado para hacer código automodificable.

Lo que suena muy interesante 😁 pero no estoy muy seguro de que cubra lo que estoy preguntando aquí.


También una pregunta relacionada: ¿cuándo entraría en vigor el segundo pase JIT? ¿Cuándo se llamará a un método por enésima vez? ¿Ocurrirá el JIT en el subproceso en el que se suponía que debía ejecutarse el método? Si es así, eso introduciría un retraso antes de la llamada al método. Si implementa optimizaciones más agresivas, este retraso sería más largo que el tiempo JIT actual, lo que puede convertirse en un problema.

Debería suceder cuando el método se llama suficientes veces, o si un ciclo
ejecuta suficientes iteraciones (reemplazo en el escenario). debería pasar
asincrónicamente en un subproceso de fondo.

El 29 de junio de 2017 a las 19:01, "Lucas Trzesniewski" [email protected]
escribió:

@noahfalk https://github.com/noahfalk , ¿podría proporcionar una forma
para decirle al tiempo de ejecución que fuerce una compilación de nivel 2 de inmediato desde el
código administrado en sí? La compilación en niveles es una muy buena idea para la mayoría
casos de uso, pero estoy trabajando en un proyecto donde el tiempo de inicio es irrelevante
pero el rendimiento y una latencia estable son esenciales.

Fuera de mi cabeza, esto podría ser:

  • una nueva configuración en el archivo de configuración, un interruptor como enable="true" /> para obligar al JIT a omitir siempre el nivel 1
  • o algo como RuntimeHelpers.PrepareMethod, que sería
    llamado por el código en todos los métodos que son parte de la ruta activa (estamos
    usando esto para pre-JIT nuestro código en el inicio). Esto tiene la ventaja de
    dando un mayor grado de libertad al desarrollador que debe saber qué
    el camino caliente es. Una sobrecarga adicional de este método estaría bien.

Por supuesto, pocos proyectos se beneficiarían de esto, pero estoy un poco preocupado por
el JIT se salta las optimizaciones de forma predeterminada, y yo no puedo decirlo
Prefiero que optimice mucho mi código.

Soy consciente de que escribiste lo siguiente en el documento de diseño:

Agregue una nueva etapa de canalización de compilación accesible desde las API de código administrado para hacer
código automodificable.

Lo cual suena muy interesante 😁 pero no estoy muy seguro de que cubra lo que

Estoy preguntando aquí.

También una pregunta relacionada: ¿cuándo entraría en vigor el segundo pase JIT? Cuando un
se llamará al método por enésima vez? ¿Sucederá el JIT en
el hilo en el que se suponía que debía ejecutarse el método? Si es así, eso introduciría una
retardo antes de la llamada al método. Si implementa más agresivo
optimizaciones, este retraso sería más largo que el tiempo JIT actual, lo que
puede convertirse en un problema.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312130920 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AGGWB2WbZ2qVBjRIQWS86MStTSa1ODfoks5sJCzOgaJpZM4IHWs8
.

@ltrzesniewski - ¡Gracias por los comentarios! Ciertamente, espero que la compilación en niveles sea útil para la gran mayoría de los proyectos, pero las compensaciones pueden no ser ideales para todos los proyectos. Estuve especulando que dejaríamos una variable de entorno en su lugar para deshabilitar el jitting por niveles, en cuyo caso mantendría el comportamiento de tiempo de ejecución que tiene ahora con mayor calidad (pero más lento para generar) jitting por adelantado. ¿Configurar una variable de entorno es algo razonable para su aplicación? También son posibles otras opciones, solo gravito hacia la variable de entorno porque es una de las opciones de configuración más simples que podemos usar.

También una pregunta relacionada: ¿cuándo entraría en vigor el segundo pase JIT?

Esta es una política que es muy probable que evolucione con el tiempo. La implementación del prototipo actual utiliza una política simplista: "¿Se ha llamado al método >= 30 veces"?
https://github.com/dotnet/coreclr/blob/master/src/vm/tieredcompilation.cpp#L89
https://github.com/dotnet/coreclr/blob/master/src/vm/tieredcompilation.cpp#L122

Convenientemente, esta política muy simple sugiere una buena mejora en el rendimiento de mi máquina, incluso si es solo una suposición. Para crear mejores políticas, necesitamos obtener comentarios sobre el uso del mundo real, y obtener esos comentarios requerirá que la mecánica central sea razonablemente sólida en una variedad de escenarios. Así que mi plan es mejorar primero la robustez/compatibilidad y luego hacer más exploración para la política de ajuste.

@DemiMarie : no tenemos nada que rastree las iteraciones de bucle como parte de la política ahora, pero es una perspectiva interesante para el futuro.

¿Ha habido alguna idea sobre la creación de perfiles, la optimización especulativa y
desoptimización? La JVM hace todo esto.

El 29 de junio de 2017 a las 8:58 p. m., "Noah Falk" [email protected] escribió:

@ltrzesniewski https://github.com/ltrzesniewski - Gracias por el
¡reacción! Ciertamente, espero que la compilación por niveles sea útil para la gran
mayoría de los proyectos, pero las ventajas y desventajas pueden no ser ideales para todos los proyectos.
He estado especulando que dejaríamos una variable de entorno en su lugar para
deshabilite el jitting escalonado, en cuyo caso mantiene el comportamiento de tiempo de ejecución que
tener ahora con mayor calidad (pero más lento para generar) jitting por adelantado. Es
establecer una variable de entorno algo razonable para su aplicación?
También son posibles otras opciones, solo gravito hacia el medio ambiente.
variable porque es una de las opciones de configuración más sencillas que podemos utilizar.

También una pregunta relacionada: ¿cuándo entraría en vigor el segundo pase JIT?

Esta es una política que es muy probable que evolucione con el tiempo. La corriente
implementación prototipo utiliza una política simplista: "¿Ha sido el método
llamado >= 30 veces"
https://github.com/dotnet/coreclr/blob/master/src/vm/
compilación por niveles.cpp#L89
https://github.com/dotnet/coreclr/blob/master/src/vm/
compilación por niveles.cpp#L122

Convenientemente, esta política muy simple sugiere una buena mejora del rendimiento en
mi máquina, incluso si es solo una suposición. Para crear mejores políticas
necesitamos obtener comentarios sobre el uso del mundo real y obtener esos comentarios
requerirá que la mecánica central sea razonablemente robusta en una variedad de
escenarios. Así que mi plan es mejorar la robustez/compatibilidad primero y luego hacer
más exploración para la política de ajuste.

@DemiMarie https://github.com/demimarie - No tenemos nada que
rastrea las iteraciones de bucle como parte de la política ahora, pero es un interesante
perspectiva para el futuro.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312146470 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AGGWB5m2qCnOKJsaXFCFigI3J6Ql8PMQks5sJEgZgaJpZM4IHWs8
.

@noahfalk Una variable ambiental definitivamente no es una solución que le permita controlar esta aplicación por aplicación. Para las aplicaciones de servidor/servicio, generalmente no le importa cuánto tiempo tarda la aplicación en iniciarse (sé que no lo hacemos a expensas del rendimiento). Al desarrollar un motor de base de datos, puedo decirles de primera mano que necesitamos que funcione lo más rápido posible desde el principio e incluso en rutas no excepcionales o puntos de referencia realizados por nuevos clientes potenciales.

Por otro lado, dado que en entornos típicos el tiempo de actividad se puede medir en semanas a la vez, no nos importa si toma incluso 30 segundos; lo que sí nos importa es que obligar al usuario a emitir un cambio general (todo o nada) o incluso hacer que el usuario tenga que preocuparse por ello (como se establece de forma predeterminada desde los archivos de configuración) es 10 pasos hacia atrás.

No me malinterpreten, espero más que un JIT en niveles porque abre el camino de un alto rendimiento, tome todo el tiempo que necesite para la ruta de código para la optimización en el nivel JIT. Incluso lo sugerí yo mismo hace mucho tiempo en conversaciones informales con algunos de los ingenieros del JIT, y ustedes ya lo tenían en el radar. Pero una forma de personalizar el comportamiento en toda la aplicación (no en todo el sistema) es (al menos para nosotros) un indicador de calidad crítico para esta función en particular.

EDITAR: algunos problemas de estilo.

@redknightlois - Gracias por el seguimiento

Una variable ambiental definitivamente no es una solución que le permita controlar esta aplicación por aplicación.

Un poco confundido en esta parte... las variables de entorno tienen una granularidad por proceso en lugar de por sistema, al menos en las plataformas que conocía. Por ejemplo, hoy, para activar la compilación por niveles para realizar pruebas en una sola aplicación que ejecuto:

set COMPLUS_EXPERIMENTAL_TieredCompilation=1
MyApp.exe
set COMPLUS_EXPERIMENTAL_TieredCompilation=0

lo que sí nos importa es que [no] obliguemos al usuario... a preocuparse por ello

Supongo que le gustaría un ajuste de configuración que pueda especificar el desarrollador de la aplicación, no la persona que ejecuta la aplicación. Una posibilidad con env var es hacer que la aplicación, el usuario inicie un contenedor trivial (como un script por lotes) que inicie la aplicación coreclr, aunque admito que parece un poco poco elegante. Estoy abierto a alternativas y no me fijo en la variable env. Solo para establecer expectativas, esta no es un área en la que dedicaré un esfuerzo de diseño activo en un futuro muy cercano, pero estoy de acuerdo en que es importante llegar a tener la configuración adecuada.

También un aviso: suponiendo que continuamos por la ruta de compilación por niveles de manera decente, podría imaginar fácilmente que llegamos a un punto en el que habilitar la compilación por niveles no solo es el inicio más rápido, sino que también supera el rendimiento de estado estable actual. En este momento, el rendimiento de inicio es mi objetivo, pero no es el límite de lo que podemos hacer con él:)

¿Ha habido alguna idea sobre la creación de perfiles, la optimización especulativa y
desoptimización?

@DemiMarie : ciertamente han surgido en las conversaciones y creo que muchas personas están emocionadas de que la compilación en niveles abra estas posibilidades. Hablando solo por mí mismo, estoy tratando de mantenerme enfocado en ofrecer las capacidades básicas de compilación por niveles antes de poner mis miras más altas. Es probable que otras personas de nuestra comunidad ya me estén adelantando en otras aplicaciones.

@noahfalk Sí, ser poco elegante también significa que el proceso habitual para ejecutarlo puede (y muy probablemente lo hará) volverse propenso a errores y ese es esencialmente el problema (la única forma de estar completamente seguro de que nadie se equivocará es hacerlo en todo el sistema). Una alternativa que sabemos que funciona es que de la misma manera puedes configurar si vas a usar el servidor GC con una entrada en el app.config puedes hacer lo mismo con la compilación por capas (al menos hasta el en niveles puede superar constantemente el rendimiento de estado estable). Al ser el JIT, también puede hacerlo por ensamblaje usando assembly.config y le daría un grado de capacidades que actualmente no existe si también se pueden seleccionar otras perillas de esa manera.

Las variables de entorno a menudo se establecen por usuario o por sistema, lo que tiene el efecto negativo potencial de afectar todos esos procesos, en múltiples versiones del tiempo de ejecución. Un archivo de configuración por aplicación parece ser una solución mucho mejor (incluso si también está disponible por usuario/por sistema), algo así como los valores de configuración del escritorio que se pueden establecer en app.config, pero también usar env vars o registro. .

Creo que implementaremos la ruta más común, que es por aplicación. La configuración de todo el sistema también puede ser útil, pero no creo que tengamos que pensar en ello antes de implementar la función.

Tenga en cuenta que no hemos elaborado en detalle qué debe hacer el jit de segundo nivel para la optimización, aunque tenemos algunas ideas. Podría hacer lo que hace el jit hoy, pero es muy probable que haga más.

Así que permítanme señalar algunas posibles complicaciones....

Es posible que el jit de segundo nivel se arranque sobre las observaciones realizadas sobre el comportamiento del código creado por el jit de primer nivel. Por lo tanto, pasar por alto el jit de primer nivel y solicitar el jit de segundo nivel directamente puede no funcionar en absoluto, o puede no funcionar tan bien como dejar que la organización en niveles siga su curso. Posiblemente, una opción de "omisión por niveles", independientemente de cómo se implemente, terminaría dando un código como el código que produce el jit de forma predeterminada hoy, no el código que podría producir un jit de segundo nivel.

El jit de segundo nivel se puede ajustar de tal manera que ejecutarlo en un gran conjunto de métodos provoque tiempos de jit relativamente lentos (ya que nuestra expectativa es que relativamente pocos métodos terminarán siendo jit con el jit de segundo nivel, y esperamos el jit de segundo nivel hará una optimización más completa). Todavía no conocemos las compensaciones correctas aquí.

Habiendo dicho eso...

Creo que un atributo de método de "optimización agresiva" tiene sentido: uno que le pida al jit que se comporte de la misma manera que el jit de segundo nivel podría comportarse para métodos específicos, y tal vez omitir estos métodos durante el prejit (ya que el código prejit se ejecuta más lento que el código jit, especialmente para R2R). Pero aplicar esta noción a un ensamblado completo oa todos los ensamblados en una aplicación no parece tan atractivo.

Si toma lo que sucede en los compiladores nativos como una analogía adecuada, las compensaciones entre el rendimiento y el tiempo de compilación/tamaño del código pueden ser bastante malas en niveles de optimización más altos, por ejemplo, compilaciones 10 veces más largas para una mejora agregada del 1-2% en el rendimiento. La clave del rompecabezas es saber qué métodos importan, y la única forma de hacerlo es que los programadores lo sepan o que el sistema lo descubra por sí mismo.

@AndyAyersMS Creo que diste en el clavo. El JIT que trata el atributo de "optimización agresiva" probablemente resolvería la mayoría de los problemas de no poder tener suficiente información para que el JIT produzca un código mejor aislado sin que el jit de primer nivel tenga tiempo para proporcionar esa retroalimentación.

El atributo @redknightlois no funcionará si queremos más niveles: - T3 JIT, T4 JIT, ... No estoy seguro de si dos niveles no son suficientes, pero al menos deberíamos considerar esta posibilidad.

Sería genial poder usar algo similar a MPGO para comenzar a ejecutar con código jitted de segundo nivel. Avance rápido del primer nivel en lugar de omitirlo por completo.

@AndyAyersMS , ¿el hecho de que Azul haya implementado un JIT administrado para JVM usando LLVM hizo que sea más fácil integrar LLVM en CLR? Aparentemente, los cambios se enviaron aguas arriba a LLVM en el proceso.

Solo para tu información, creé una serie de elementos de trabajo para un trabajo en particular que debemos hacer para que los niveles despeguen (#12609, dotnet/coreclr#12610, dotnet/coreclr#12611, dotnet/coreclr#12612, dotnet/coreclr #12617). Si su interés se relaciona directamente con uno de esos, siéntase libre de agregar sus comentarios. Para cualquier otro tema, asumo que la discusión permanecerá aquí, o cualquiera puede crear un problema para un subtema específico si hay suficiente interés como para dividirlo por sí solo.

@MendelMonteiro Hacer que los datos de retroalimentación de estilo MPGO estén disponibles cuando se realiza un jitting es ciertamente una opción (actualmente solo podemos leer estos datos cuando se realiza un prejitting). Hay varios límites a lo que se puede instrumentar, por lo que no todos los métodos se pueden manejar de esta manera, hay otras limitaciones que debemos tener en cuenta (por ejemplo, no hay datos de retroalimentación disponibles para los usuarios en línea), la instrumentación y las ejecuciones de capacitación necesarias para crear los datos de MPGO son una barrera para muchos usuarios, y los datos de MPGO pueden o no coincidir con lo que tendríamos al iniciar el primer nivel, pero la idea ciertamente tiene mérito.

En lo que respecta a un nivel superior basado en LLVM, obviamente hemos investigado esto hasta cierto punto con LLILC, y en ese momento estábamos en contacto frecuente con la gente de Azul, por lo que estamos familiarizados con muchas de las cosas que estaban haciendo en LLVM para hacerlo más susceptible a la compilación de idiomas con GC preciso.

Hubo (y probablemente todavía hay) diferencias significativas en el soporte de LLVM necesario para CLR frente a lo que se necesita para Java, tanto en GC como en EH, y en las restricciones que se deben aplicar al optimizador. Para citar solo un ejemplo: el GC de CLR actualmente no puede tolerar punteros administrados que señalen el final de los objetos. Java maneja esto a través de un mecanismo de informes emparejados base/derivado. Necesitaríamos instalar soporte para este tipo de informes emparejados en CLR o restringir los pases del optimizador de LLVM para nunca crear este tipo de punteros. Además de eso, el jit de LLILC era lento y, en última instancia, no estábamos seguros de qué tipo de calidad de código podría producir.

Por lo tanto, descubrir cómo LLILC podría encajar en un posible enfoque de varios niveles que aún no existía parecía (y sigue pareciendo) prematuro. La idea por ahora es obtener niveles en el marco y usar RyuJit para el jit de segundo nivel. A medida que aprendemos más, podemos descubrir que, de hecho, hay espacio para jits de nivel superior o, al menos, comprender mejor qué más debemos hacer antes de que tales cosas tengan sentido.

@AndyAyersMS Tal vez pueda introducir los cambios necesarios en LLVM y evitar sus limitaciones.

¿ Multicore JIT y su Profile Optimization funcionan con coreclr?

@benaadams - Sí, JIT multinúcleo funciona. No recuerdo en qué escenarios (si los hay) está habilitado de forma predeterminada, pero puede activarlo a través de la configuración: https://github.com/dotnet/coreclr/blob/master/src/inc/clrconfigvalues.h# L548

Escribí un compilador de medio juguete y me di cuenta de que la mayoría de las veces las optimizaciones de gran impacto se pueden hacer bastante bien en la misma infraestructura y se pueden hacer muy pocas cosas en el optimizador de nivel superior.

Lo que quiero decir es esto: si una función se golpea muchas veces, los parámetros son:

  • aumentar el número de instrucciones en línea
  • use un asignador de registro más "avanzado" (colorador de retroceso similar a LLVM o colorizador completo)
  • haga más pases de optimizaciones, tal vez algunos especializados con el conocimiento local. Por ejemplo: permita reemplazar la asignación completa de objetos en la asignación de pila si el objeto se declara en el método y no se asigna en el cuerpo de una función en línea más grande.
  • use PIC para la mayoría de los objetos golpeados donde CHA no es posible. Incluso StringBuilder, por ejemplo, es muy probable que no se anule, el código podría marcarse como cada vez que se golpeó con un StringBuilder, todos los métodos llamados en el interior se pueden desvirtualizar de forma segura y se establece una protección de tipo frente al acceso de SB.

También sería muy bueno, pero tal vez este es mi sueño despierto, que CompilerServices ofrezca el "compilador avanzado" para estar expuesto y poder acceder a él a través de código o metadatos, por lo que lugares como juegos o plataformas comerciales podrían beneficiarse al comenzar compilación antes de tiempo qué clases y métodos se "compilarán más profundamente". Esto no es NGen, pero si un compilador sin niveles no es necesariamente posible (deseable), al menos será posible usar el código optimizado más pesado para partes críticas que necesitan este rendimiento adicional. Por supuesto, si una plataforma no ofrece las optimizaciones pesadas (digamos Mono), las llamadas API serán básicamente NO-OP.

Ahora tenemos una base sólida para colocar en niveles gracias al arduo trabajo de @noahfalk , @kouvel y otros.

Sugiero que cerremos este problema y abramos un problema de "¿cómo podemos mejorar el jitting escalonado?". Animo a cualquier persona interesada en el tema a que pruebe los niveles actuales para tener una idea de dónde están las cosas en este momento. Nos encantaría recibir comentarios sobre el comportamiento real, ya sea bueno o malo.

¿Se describe el comportamiento actual en alguna parte? Solo encontré esto , pero se trata más de los detalles de implementación que de la organización en niveles específicamente.

Creo que vamos a tener algún tipo de resumen escrito disponible pronto, con algunos de los datos que hemos recopilado.

La clasificación por niveles se puede habilitar en 2.1 configurando COMPlus_TieredCompilation=1 . Si lo intentas, por favor informa lo que encuentres....

Con relaciones públicas recientes (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), también tiene la capacidad de especificar la compilación en niveles como una configuración de tiempo de ejecución. json o una propiedad de proyecto de msbuild. El uso de esta funcionalidad requerirá que esté en compilaciones muy recientes, mientras que la variable de entorno ha existido por un tiempo.

Como hemos discutido antes con @jkotas , Tiered JIT puede mejorar el tiempo de inicio. ¿Funciona cuando usamos imágenes nativas?
Hemos realizado mediciones para varias aplicaciones en el teléfono Tizen y estos son los resultados:

DLL del sistema|DLL de la aplicación|En niveles|tiempo, s
-----------|--------|-------|--------
R2R |R2R |no |2.68
R2R |R2R |sí |2,61 (-3%)
R2R |no |no |4.40
R2R |no |si |3.63 (-17%)

También comprobaremos el modo FNV, pero parece que funciona bien cuando no hay imágenes.

cc @gbalykov @nkaretnikov2

FYI, la compilación en niveles ahora es la predeterminada para .NET Core: https://github.com/dotnet/coreclr/pull/19525

@alpencolt , las mejoras en el tiempo de inicio pueden ser menores cuando se usa la compilación AOT como R2R. Actualmente, la mejora en el tiempo de inicio proviene de realizar jitting más rápidamente con menos optimizaciones, y cuando se utiliza la compilación AOT, habría menos para JIT. Algunos métodos no están pregenerados, como algunos genéricos, stubs de IL y otros métodos dinámicos. Algunos genéricos pueden beneficiarse de la organización en niveles durante el inicio, incluso cuando se utiliza la compilación AOT.

Seguiré adelante y cerraré este problema, ya que con el compromiso de @kouvel creo que he logrado lo que se pide en el título: D Las personas pueden continuar la discusión y/o abrir nuevos problemas sobre temas más específicos, como las mejoras solicitadas, preguntas o investigaciones particulares. Si alguien piensa que está cerrado prematuramente, por supuesto, háganoslo saber.

@kouvel Perdón por comentar sobre el tema cerrado. Me pregunto cuando se usa la compilación AOT como crossgen, ¿la aplicación seguirá beneficiándose de la compilación de segundo nivel para las rutas de código de puntos calientes?

@daxian-dbw sí, mucho; en el tiempo de ejecución, Jit puede hacer interlineado de ensamblaje cruzado (entre dlls); eliminación de ramas basada en constantes de tiempo de ejecución ( readonly static ); etc.

@benaadams ¿Y un compilador AOT bien diseñado no podría?

Encontré información sobre esto en https://blogs.msdn.microsoft.com/dotnet/2018/08/02/tiered-compilation-preview-in-net-core-2-1/ :

las imágenes precompiladas tienen restricciones de versión y restricciones de instrucciones de CPU que prohíben algunos tipos de optimización. Para cualquier método en estas imágenes que se llame con frecuencia, la compilación en niveles solicita al JIT que cree código optimizado en un subproceso de fondo que reemplazará la versión precompilada.

Sí, ese es un ejemplo de "un AOT no bien diseñado". 😛

las imágenes precompiladas tienen restricciones de versión y restricciones de instrucciones de CPU que prohíben algunos tipos de optimización.

Uno de los ejemplos son los métodos que utilizan hardware intrínseco. El compilador AOT (crossgen) solo asume SSE2 como el objetivo de generación de código en x86/x64, por lo que todos los métodos que usan hardware intrínseco serán rechazados por crossgen y compilados por JIT que conoce la información de hardware subyacente.

¿Y un compilador AOT bien diseñado no podría?

El compilador AOT necesita optimización de tiempo de enlace (para la inserción de ensamblajes cruzados) y optimización guiada por perfil (para constantes de tiempo de ejecución). Mientras tanto, el compilador AOT necesita una información de hardware "resultante" (como -mavx2 en gcc/clang) en tiempo de compilación para el código SIMD.

Uno de los ejemplos son los métodos que utilizan hardware intrínseco. El compilador AOT (crossgen) solo asume SSE2 como el objetivo de generación de código en x86/x64, por lo que todos los métodos que usan hardware intrínseco serán rechazados por crossgen y compilados por JIT que conoce la información de hardware subyacente.

¿Esperar lo? No sigo muy bien aquí. ¿Por qué el compilador AOT rechazaría los intrínsecos?

¿Y un compilador AOT bien diseñado no podría?

El compilador AOT necesita optimización de tiempo de enlace (para la inserción de ensamblajes cruzados) y optimización guiada por perfil (para constantes de tiempo de ejecución). Mientras tanto, el compilador AOT necesita una información de hardware "resultante" (como -mavx2 en gcc/clang) en tiempo de compilación para el código SIMD.

Sí, como dije, "un compilador AOT bien diseñado". 😁

@masonwheeler escenario diferente; crossgen es AoT que funciona con Jit y permite el mantenimiento/parcheo de dlls sin necesidad de volver a compilar y redistribuir la aplicación completa. Ofrece una mejor generación de código que Tier0 con un inicio más rápido que Tier1; pero no es una plataforma neutral.

Tier0, crossgen y Tier1 funcionan juntos como un modelo cohesivo en coreclr

Para realizar un ensamblaje cruzado en línea (no Jit), se requeriría la compilación de un solo archivo ejecutable vinculado estáticamente y se requeriría una recompilación completa y una redistribución de la aplicación para parchear cualquier biblioteca que usara, así como también apuntar a la plataforma específica (qué versión de SSE, Avx, etc. para usar; ¿la versión más baja común o producir para todos?).

corert AoT este estilo de aplicación.

Sin embargo; hacer ciertos tipos de eliminación de ramas que el Jit puede hacer requeriría una gran cantidad de generación adicional de asm para las rutas alternativas; y el parche de tiempo de ejecución del árbol correcto

por ejemplo, cualquier código que use un método como (donde Tier1 Jit eliminará todos los if s)

readonly static _numProcs = Environment.ProcessorCount;

public void DoThing()
{
    if (_numProcs == 1) 
    {
       // Single proc path
    }
    else if (_numProcs == 2) 
    {
       // Two proc path
    }
    else
    {
       // Multi proc path
    }
}

@benaadams

Para realizar un ensamblaje cruzado en línea (no Jit), se requeriría la compilación de un solo archivo ejecutable vinculado estáticamente y se requeriría una recompilación completa y una redistribución de la aplicación para parchear cualquier biblioteca que usara, así como también apuntar a la plataforma específica (qué versión de SSE, Avx, etc. para usar; ¿la versión más baja común o producir para todos?).

No debería requerir una redistribución completa de la aplicación. Mire el sistema de compilación ART de Android: distribuye la aplicación como código administrado (Java en su caso, pero se aplican los mismos principios) y el compilador, que vive en el sistema local, AOT compila el código administrado en un ejecutable nativo súper optimizado.

Si cambia una pequeña biblioteca, todo el código administrado todavía está allí y no tendría que redistribuirlo todo, solo el parche, y luego el AOT se puede volver a ejecutar para producir un nuevo ejecutable. (Obviamente, aquí es donde se rompe la analogía con Android, debido al modelo de distribución de aplicaciones APK de Android, pero eso no se aplica al desarrollo de escritorio/servidor).

y el compilador, que vive en el sistema local, AOT compila el código administrado...

Ese es el modelo NGen anterior que utilizó el marco completo; aunque tampoco crea que creó un solo ensamblaje que inserta el código del marco en el código de las aplicaciones. ¡La diferencia entre dos enfoques se destacó en las ejecuciones de Bing.com en .NET Core 2.1! entrada en el blog

Imágenes listas para ejecutar

Las aplicaciones administradas a menudo pueden tener un rendimiento de inicio deficiente, ya que los métodos primero deben compilarse JIT en código de máquina. .NET Framework tiene una tecnología de precompilación, NGEN. Sin embargo, NGEN requiere que el paso de precompilación ocurra en la máquina en la que se ejecutará el código. Para Bing, eso significaría NGENing en miles de máquinas. Esto, junto con un ciclo de implementación agresivo, daría como resultado una reducción significativa de la capacidad de servicio a medida que la aplicación se precompila en las máquinas de servicio web. Además, la ejecución de NGEN requiere privilegios administrativos, que a menudo no están disponibles o se analizan minuciosamente en un entorno de centro de datos. En .NET Core, la herramienta crossgen permite precompilar el código como un paso previo a la implementación, como en el laboratorio de compilación, ¡y las imágenes implementadas en producción están listas para ejecutarse!

@masonwheeler AOT enfrenta vientos en contra en .Net completo debido a la naturaleza dinámica de un proceso .Net. Por ejemplo, los cuerpos de métodos en .Net se pueden modificar a través de un generador de perfiles en cualquier momento, las clases se pueden cargar o crear a través de la reflexión, y el tiempo de ejecución puede crear un nuevo código según sea necesario para cosas como la interoperabilidad, por lo que la información del análisis entre procedimientos refleja lo mejor posible. un estado transitorio del proceso en ejecución. Cualquier análisis u optimización entre procedimientos (incluida la inserción) en .Net se debe poder deshacer en tiempo de ejecución.

AOT funciona mejor cuando el conjunto de cosas que pueden cambiar entre el tiempo de AOT y el tiempo de ejecución es pequeño y el impacto de dichos cambios está localizado, de modo que el alcance expansivo disponible para la optimización de AOT refleje en gran medida cosas que siempre deben ser ciertas (o quizás tener un pequeño número de alternativas).

Si puede crear mecanismos para hacer frente o restringir la naturaleza dinámica de los procesos .Net, entonces AOT puro puede funcionar bastante bien; por ejemplo, .Net Native considera el impacto de la reflexión y la interoperabilidad, y prohíbe la carga de ensamblados, la emisión de reflexión y ( Supongo) perfil adjunto. Pero no es sencillo.

Hay algo de trabajo en marcha que nos permite expandir el alcance de crossgen a múltiples ensamblajes para que podamos compilar AOT todos los marcos principales (o todos los ensamblajes de asp.net) como un paquete. Pero eso solo es viable porque tenemos el jit como respaldo para rehacer la generación de código cuando las cosas cambian.

@AndyAyersMS Nunca creí que la solución .NET AOT debería ser una solución "solo AOT pura", exactamente por las razones que está describiendo aquí. Es muy importante contar con el JIT para crear código nuevo según sea necesario. Pero las situaciones en las que se necesita son muy minoritarias y, por lo tanto, creo que la regla de Anders Hejlsberg para los sistemas de tipos podría aplicarse aquí de manera rentable:

Estático donde sea posible, dinámico cuando sea necesario.

Desde System.Linq.Expresiones
public TDelegate Compile(bool preferInterpretation);

¿Continúa funcionando la compilación por niveles si preferInterpretation es verdadero?

¿Fue útil esta página
0 / 5 - 0 calificaciones