Runtime: Introduzir um JIT em camadas

Criado em 14 abr. 2016  ·  63Comentários  ·  Fonte: dotnet/runtime

Por que o .NET JIT não está em camadas?

O JIT tem dois objetivos principais de projeto: tempo de inicialização rápido e alta taxa de transferência em estado estável.

A princípio, esses objetivos aparecem em desacordo. Mas com um design JIT de duas camadas, ambos são atingíveis:

  1. Todo o código começa interpretado. Isso resulta em um tempo de inicialização extremamente rápido (mais rápido que o RyuJIT). Exemplo: O método Main é quase sempre frio e descartá-lo é uma perda de tempo.
  2. O código que é executado com frequência é alterado usando um gerador de código de alta qualidade. Muito poucos métodos serão quentes (1%?). Portanto, o rendimento do JIT de alta qualidade não importa muito. Ele pode gastar tanto tempo quanto um compilador C gasta para gerar um código muito bom. Além disso, pode _assumir_ que o código está quente. Pode inline como louco e desenrolar loops. O tamanho do código não é uma preocupação.

Alcançar essa arquitetura não parece muito caro:

  1. Escrever um intérprete parece barato comparado a um JIT.
  2. Um gerador de código de alta qualidade deve ser criado. Isso poderia ser VC, ou o projeto LLILC.
  3. Deve ser possível fazer a transição do código em execução interpretado para o código compilado. Isso é possível; a JVM faz isso. É chamado de substituição de pilha (OSR).

Essa ideia está sendo perseguida pela equipe JIT?

O .NET é executado em centenas de milhões de servidores. Eu sinto que muito desempenho é deixado de lado e milhões de servidores são desperdiçados para os clientes por causa da geração de código abaixo do ideal.

categoria: taxa de transferência
tema: grandes apostas
nível de habilidade: especialista
custo: extra grande

area-CodeGen-coreclr enhancement optimization tenet-performance

Comentários muito úteis

Com PRs recentes (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), você também pode especificar a compilação em camadas como um runtimeconfig. json ou uma propriedade de projeto msbuild. O uso dessa funcionalidade exigirá que você esteja em compilações muito recentes, enquanto a variável de ambiente já existe há algum tempo.

Todos 63 comentários

@GSPP Tiering é um tópico constante nas conversas de planejamento. Minha impressão é que é uma questão de _quando_, não _se_, se isso traz algum consolo. Quanto ao _por que_ ainda não está lá, acho que é porque, historicamente, os ganhos potenciais percebidos não justificavam os recursos de desenvolvimento adicionais necessários para gerenciar o aumento da complexidade e risco de vários modos de codegen. Eu realmente deveria deixar os especialistas falarem sobre isso, então vou adicioná-los.

/cc @dotnet/jit-contrib @russellhadley

De alguma forma, duvido que isso ainda seja relevante em um mundo de crossgen/ngen, Ready to Run e corret.

Nenhum deles oferece alta taxa de transferência de estado estável no momento, o que é importante para a maioria dos aplicativos da web. Se eles o fizerem, fico feliz com isso, pois pessoalmente não me importo com o tempo de inicialização.

Mas até agora todos os geradores de código para .NET tentaram fazer um ato de equilíbrio impossível entre os dois objetivos, não cumprindo muito bem nenhum dos dois. Vamos nos livrar desse ato de equilíbrio para que possamos transformar as otimizações em 11.

Mas até agora todos os geradores de código para .NET tentaram fazer um ato de equilíbrio impossível entre os dois objetivos, não cumprindo muito bem nenhum dos dois. Vamos nos livrar desse ato de equilíbrio para que possamos transformar as otimizações em 11.

Eu concordo, mas consertar isso não requer coisas como um intérprete. Apenas um bom compilador crossgen, seja um melhor RyuJIT ou LLILC.

Acho que a maior vantagem é para aplicativos que precisam gerar código em tempo de execução. Isso inclui linguagens dinâmicas e contêineres de servidor.

É verdade que o código gerado dinamicamente é uma motivação - mas também é verdade que um compilador estático nunca terá acesso a todas as informações disponíveis em tempo de execução. Além disso, mesmo quando especula (por exemplo, com base em informações de perfil), é muito mais difícil para um compilador estático fazê-lo na presença de comportamento dependente de contexto modal ou externo.

Os aplicativos da Web não devem precisar de nenhum processamento no estilo ngen. Ele não se encaixa bem no pipeline de implantação. Leva muito tempo para gerar um binário grande (mesmo que quase todo o código esteja dinamicamente morto ou frio).

Além disso, ao depurar e testar um aplicativo da Web, você não pode confiar no ngen para fornecer um desempenho realista.

Além disso, eu defendo o ponto de Carol de usar informações dinâmicas. A camada de interpretação pode criar perfil de código (ramificações, contagens de loop trip, destinos de despacho dinâmicos). É uma combinação perfeita! Primeiro colete o perfil e depois otimize.

A classificação em camadas resolve tudo em todos os cenários para sempre. Falando aproximadamente :) Isso pode realmente nos levar à promessa dos JITs: Alcançar desempenho _além_ do que um compilador C pode fazer.

A implementação atual do RyuJIT como está agora é boa o suficiente para um Tier 1... A questão é: faria sentido ter um JIT de otimização extrema Tier 2 para hot paths que podem ser executados após o fato? Essencialmente, quando detectamos ou temos informações de tempo de execução suficientes para saber que algo está quente ou quando solicitados a usar isso desde o início.

RyuJIT é de longe bom o suficiente para ser o nível 1. O problema com isso é que um interpretador teria um tempo de inicialização _muito_ mais rápido (na minha estimativa). O segundo problema é que, para avançar para a camada 2, o estado local de execução do código da camada 1 deve ser transferível para o novo código da camada 2 (OSR). Isso requer mudanças no RyuJIT. Adicionar um interpretador seria, eu acho, um caminho mais barato com melhor latência de inicialização ao mesmo tempo.

Uma variante ainda mais barata seria não substituir o código em execução pelo código da camada 2. Em vez disso, espere até que o código da camada 1 retorne naturalmente. Isso pode ser um problema se o código entrar em um hot loop de longa duração. Ele nunca chegará ao desempenho de nível 2 dessa maneira.

Acho que isso não seria tão ruim e poderia ser usado como uma estratégia v1. Idéias de mitigação estão disponíveis, como um atributo marcando um método como quente (isso deve existir de qualquer maneira, mesmo com a estratégia JIT atual).

@GSPP Isso é verdade, mas isso não significa que você não saberia disso na próxima execução. Se o código e a instrumentação Jitted se tornarem persistentes, na segunda execução você ainda obterá o código de Camada 2 (às custas de algum tempo de inicialização) --- o que, pela primeira vez, pessoalmente não me importo, pois escrevo principalmente código de servidor.

Escrever um intérprete parece barato comparado a um JIT.

Em vez de escrever um novo interpretador, faria sentido executar o RyuJIT com as otimizações desabilitadas? Isso melhoraria o tempo de inicialização o suficiente?

Um gerador de código de alta qualidade deve ser criado. Isso pode ser vc

Você está falando de C2, o back-end do Visual C++? Isso não é multiplataforma e nem código aberto. Duvido que consertar ambos aconteceria tão cedo.

Boa ideia com a desativação de otimizações. O problema OSR permanece, no entanto. Não tenho certeza de como é difícil gerar código que permita que o tempo de execução derive o estado de arquitetura de IL (locais e pilha) em tempo de execução em um ponto seguro, copie isso no código jitted de camada 2 e retome a função intermediária de execução de camada 2. A JVM faz isso, mas quem sabe quanto tempo levou para implementar isso.

Sim, eu estava falando sobre C2. Acho que lembro que pelo menos um dos Desktop JITs é baseado em código C2. Provavelmente não funciona para CoreCLR, mas talvez para Desktop. Tenho certeza de que a Microsoft está interessada em ter bases de código alinhadas, então isso provavelmente está de fato fora. LLVM parece ser uma ótima escolha. Acredito que vários idiomas estão atualmente interessados ​​em fazer o LLVM funcionar com GCs e com runtimes gerenciados em geral.

LLVM parece ser uma ótima escolha. Acredito que vários idiomas estão atualmente interessados ​​em fazer o LLVM funcionar com GCs e com runtimes gerenciados em geral.

Um artigo interessante sobre este tópico: a Apple recentemente mudou a camada final de seu JavaScript JIT para longe do LLVM: https://webkit.org/blog/5852/introducing-the-b3-jit-compiler/ . Provavelmente encontraríamos problemas semelhantes ao que eles encontraram: tempos de compilação lentos e falta de conhecimento do LLVM sobre o idioma de origem.

10x mais lento que RyuJIT seria totalmente aceitável para um 2º nível.

Não acho que a falta de conhecimento da linguagem fonte (que é uma verdadeira preocupação) seja inerente à arquitetura do LLVM. Acredito que várias equipes estão ocupadas movendo o LLVM para um estado em que o conhecimento do idioma de origem possa ser utilizado mais facilmente. _Todas_ as linguagens de alto nível não-C têm esse problema ao compilar no LLVM.

O projeto WebKIT FTL/B3 está em uma posição mais difícil de ser bem-sucedido do que o .NET porque eles devem se destacar ao executar o código que _no total_ consome algumas centenas de milissegundos de tempo e depois sai. Essa é a natureza das cargas de trabalho JavaScript que conduzem páginas da Web. .NET não está nesse ponto.

@GSPP Tenho certeza que você provavelmente conhece o LLILC . Se não, dê uma olhada.

Estamos trabalhando há algum tempo no suporte LLVM para conceitos CLR e investimos em melhorias de EH e GC. Ainda um pouco mais a fazer em ambos. Além disso, há uma quantidade desconhecida de trabalho para fazer as otimizações funcionarem corretamente na presença do GC.

LLILC parece estar parado. É isso?
Em 18 de abril de 2016, 19h32, "Andy Ayers" [email protected] escreveu:

@GSPP https://github.com/GSPP Tenho certeza que você provavelmente conhece o LLILC
https://github.com/dotnet/llilc. Se não, dê uma olhada.

Estamos trabalhando há algum tempo no suporte do LLVM para conceitos CLR e temos
investido em melhorias de EH e GC. Ainda há um pouco mais a fazer em
Ambas. Além disso, há uma quantidade desconhecida de trabalho para obter otimizações
funcionando corretamente na presença de GC.


Você está recebendo isso porque comentou.
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment -211630483

@drbo - LLILC está em segundo plano no momento - a equipe da MS tem se concentrado em obter mais alvos no RyuJIT, bem como corrigir problemas que surgem quando as unidades CoreCLR são lançadas e isso levou praticamente todo o nosso tempo. Está na minha lista de TODO (no meu copioso tempo livre) escrever um post de lições aprendidas com base em quão longe nós (atualmente) chegamos com o LLILC, mas ainda não cheguei a isso.
Sobre o tiering, este tópico gerou muita discussão ao longo dos anos. Acho que, dadas algumas das novas cargas de trabalho, bem como a nova adição de imagens prontas para execução em versão, vamos dar uma nova olhada em como e onde colocar os níveis.

@russellhadley você teve tempo livre para escrever o post?

Suponho que deve haver algo sobre slots de pilha não promovidos e gcroots quebrando as opções e tempo de jitting lento... É melhor eu dar uma olhada no código do projeto.

Também me pergunto se é possível e lucrativo pular diretamente para o SelectionDAG e executar parte do back-end do LLVM. Pelo menos algum peephole e propagação de cópia ... se, por exemplo, a promoção gcroot para os registros for suportada no LLILC

Estou curioso sobre o status do LLILC, incluindo gargalos atuais e como ele se sai contra o RyuJIT. O LLVM sendo um compilador de "força industrial" completo deve ter uma grande variedade de otimizações disponíveis para OSS. Houve algumas conversas sobre serialização/desserialização mais eficiente e rápida do formato de código de bits na lista de discussão; Eu estou querendo saber se isso é uma coisa útil para LLILC.

Houve mais alguma reflexão sobre isso? @russellhadley CoreCLR foi lançado e RyuJIT foi portado para (pelo menos) x86 – o que vem a seguir no roteiro?

Veja dotnet/coreclr#10478 para o início do trabalho sobre isso.

Também dotnet/coreclr#12193

@noahfalk , você poderia fornecer uma maneira de informar ao tempo de execução para forçar uma compilação de nível 2 imediatamente do próprio código gerenciado? A compilação em camadas é uma ideia muito boa para a maioria dos casos de uso, mas estou trabalhando em um projeto em que o tempo de inicialização é irrelevante, mas a taxa de transferência e uma latência estável são essenciais.

De cabeça, isso pode ser:

  • uma nova configuração no arquivo de configuração, um switch como <gcServer enabled="true" /> para forçar o JIT a sempre pular o nível 1
  • ou algo como RuntimeHelpers.PrepareMethod , que seria chamado pelo código em todos os métodos que fazem parte do hot path (estamos usando isso para pré-JIT nosso código na inicialização). Isso tem a vantagem de dar um grau maior de liberdade ao desenvolvedor, que deve saber qual é o hot path. Uma sobrecarga adicional desse método seria ótima.

Concedido, poucos projetos se beneficiariam disso, mas estou meio preocupado com o JIT ignorando otimizações por padrão, e não sendo capaz de dizer que prefiro otimizar meu código fortemente .

Estou ciente de que você escreveu o seguinte no documento de design:

Adicione um novo estágio de pipeline de compilação acessível a partir de APIs de código gerenciado para fazer código automodificável.

O que parece muito interessante 😁 mas não tenho certeza se cobre o que estou perguntando aqui.


Também uma pergunta relacionada: quando o segundo passe JIT entraria em ação? Quando um método será chamado pela enésima vez? O JIT acontecerá no thread em que o método deveria ser executado? Nesse caso, isso introduziria um atraso antes da chamada do método. Se você implementar otimizações mais agressivas, esse atraso será maior que o tempo JIT atual, o que pode se tornar um problema.

Isso deve acontecer quando o método é chamado vezes suficientes, ou se um loop
executa iterações suficientes (substituição no palco). Deve acontecer
de forma assíncrona em um thread em segundo plano.

Em 29 de junho de 2017 19h01, "Lucas Trzesniewski" [email protected]
escreveu:

@noahfalk https://github.com/noahfalk , você poderia fornecer uma maneira
para dizer ao tempo de execução para forçar uma compilação de nível 2 imediatamente
código gerenciado em si? A compilação em camadas é uma ideia muito boa para a maioria
casos de uso, mas estou trabalhando em um projeto em que o tempo de inicialização é irrelevante
mas a taxa de transferência e uma latência estável são essenciais.

De cabeça, isso pode ser:

  • uma nova configuração no arquivo de configuração, um switch como enabled="true" /> para forçar o JIT a sempre pular a camada 1
  • ou algo como RuntimeHelpers.PrepareMethod, que seria
    chamado pelo código em todos os métodos que fazem parte do hot path (estamos
    usando isso para pré-JIT nosso código na inicialização). Isso tem a vantagem de
    dando um maior grau de liberdade ao desenvolvedor que deve saber o que
    o caminho quente é. Uma sobrecarga adicional desse método seria ótima.

Certo, poucos projetos se beneficiariam com isso, mas estou meio preocupado com
o JIT pulando otimizações por padrão, e eu não sendo capaz de dizer
Eu prefiro que ele otimize meu código fortemente .

Estou ciente de que você escreveu o seguinte no documento de design:

Adicionar novo estágio de pipeline de compilação acessível a partir de APIs de código gerenciado para fazer
código auto-modificável.

O que parece muito interessante 😁 mas não tenho certeza se cobre o que

Estou perguntando aqui.

Também uma pergunta relacionada: quando o segundo passe JIT entraria em ação? Quando um
método será chamado pela enésima vez? O JIT acontecerá em
o segmento em que o método deveria ser executado? Se sim, isso introduziria um
atraso antes da chamada do método. Se você implementar mais agressivo
otimizações, esse atraso seria maior que o tempo JIT atual, o que
pode se tornar um problema.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312130920 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AGGWB2WbZ2qVBjRIQWS86MStTSa1ODfoks5sJCzOgaJpZM4IHWs8
.

@ltrzesniewski - Obrigado pelo feedback! Certamente espero que a compilação em camadas seja útil para a grande maioria dos projetos, mas as compensações podem não ser ideais para todos os projetos. Eu tenho especulado que deixaríamos uma variável de ambiente no lugar para desabilitar o jitting em camadas, caso em que você mantém o comportamento de tempo de execução que você tem agora com maior qualidade (mas mais lento para gerar) jitting na frente. Definir uma variável de ambiente é algo razoável para o seu aplicativo fazer? Outras opções também são possíveis, apenas gravito para a variável de ambiente porque é uma das opções de configuração mais simples que podemos usar.

Também uma pergunta relacionada: quando o segundo passe JIT entraria em ação?

Esta é uma política que provavelmente evoluirá ao longo do tempo. A implementação do protótipo atual usa uma política simplista: "O método foi chamado >= 30 vezes"
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, essa política muito simples sugere uma boa melhoria de desempenho na minha máquina, mesmo que seja apenas um palpite. Para criar políticas melhores, precisamos obter algum feedback de uso do mundo real, e obter esse feedback exigirá que a mecânica principal seja razoavelmente robusta em vários cenários. Então, meu plano é melhorar a robustez/compatibilidade primeiro e depois explorar mais a política de ajuste.

@DemiMarie - Não temos nada que rastreie iterações de loop como parte da política agora, mas é uma perspectiva interessante para o futuro.

Houve algum pensamento sobre criação de perfil, otimização especulativa e
desotimização? A JVM faz tudo isso.

Em 29 de junho de 2017 20:58, "Noah Falk" [email protected] escreveu:

@ltrzesniewski https://github.com/ltrzesniewski - Obrigado pelo
comentários! Certamente espero que a compilação em camadas seja útil para a vasta
maioria dos projetos, mas as compensações podem não ser ideais para todos os projetos.
Eu tenho especulado que deixaríamos uma variável de ambiente no lugar para
desabilite o jitting em camadas, caso em que você mantém o comportamento de tempo de execução que você
agora com maior qualidade (mas mais lenta para gerar) saltando na frente. É
definindo uma variável de ambiente algo razoável para o seu aplicativo fazer?
Outras opções também são possíveis, apenas gravito para o ambiente
variável porque é uma das opções de configuração mais simples que podemos usar.

Também uma pergunta relacionada: quando o segundo passe JIT entraria em ação?

Esta é uma política que provavelmente evoluirá ao longo do tempo. O actual
A implementação do protótipo usa uma política simplista: "O método foi
chamado >= 30 vezes"
https://github.com/dotnet/coreclr/blob/master/src/vm/
compilação em camadas.cpp#L89
https://github.com/dotnet/coreclr/blob/master/src/vm/
compilação em camadas.cpp#L122

Convenientemente, esta política muito simples sugere uma boa melhoria de desempenho em
minha máquina, mesmo que seja apenas um palpite. Para criar melhores políticas
precisamos obter algum feedback de uso do mundo real e obter esse feedback
exigirá que a mecânica central seja razoavelmente robusta em uma variedade de
cenários. Então, meu plano é melhorar a robustez/compatibilidade primeiro e depois fazer
mais exploração para a política de ajuste.

@DemiMarie https://github.com/demimarie - Não temos nada que
rastreia iterações de loop como parte da política agora, mas é um interessante
perspectiva para o futuro.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312146470 ,
ou silenciar o thread
https://github.com/notifications/unsubscribe-auth/AGGWB5m2qCnOKJsaXFCFigI3J6Ql8PMQks5sJEgZgaJpZM4IHWs8
.

@noahfalk Uma variável de ambiente definitivamente não é uma solução que permitiria controlar esse aplicativo por aplicativo. Para aplicativos de servidor/serviço, você geralmente não se importa com quanto tempo leva para o aplicativo inicializar (sei que não às custas do desempenho). Desenvolvendo um mecanismo de banco de dados, posso dizer em primeira mão, precisamos que ele funcione o mais rápido possível desde o início e até mesmo em caminhos não excepcionais ou benchmarks feitos por novos clientes em potencial.

Por outro lado, dado que em ambientes típicos o tempo de atividade pode ser medido em semanas de cada vez, não nos importamos se leva até 30 segundos; o que nos importa é que forçar o usuário a emitir um switch geral (tudo ou nada) ou mesmo fazer com que o usuário se preocupe com isso (como definido por padrão nos arquivos de configuração) é 10 passos para trás.

Não me entenda mal, estou mais do que ansioso por um JIT em camadas porque ele abre o caminho para um alto desempenho que leva o tempo que você precisa para a otimização no nível do JIT. Eu mesmo sugeri isso há muito tempo em conversas informais com alguns dos engenheiros do JIT, e você já tinha isso no radar. Mas uma maneira de personalizar o comportamento do aplicativo (não do sistema) é (pelo menos para nós) um indicador de qualidade crítico para esse recurso específico.

EDIT: Alguns problemas de estilo.

@redknightlois - Obrigado pelo acompanhamento

Uma variável de ambiente definitivamente não é uma solução que permitiria controlar esta aplicação por aplicação.

Um pouco confuso nessa parte... as variáveis ​​de ambiente têm granularidade por processo e não por sistema, pelo menos nas plataformas que eu conhecia. Por exemplo, hoje para ativar a compilação em camadas para teste em apenas um aplicativo que eu executo:

set COMPLUS_EXPERIMENTAL_TieredCompilation=1
MyApp.exe
set COMPLUS_EXPERIMENTAL_TieredCompilation=0

o que a gente se importa é que [não] forçar o usuário... a se importar com isso

Suponho que você gostaria de uma definição de configuração que possa ser especificada pelo desenvolvedor do aplicativo, não pela pessoa que executa o aplicativo? Uma possibilidade com o env var é fazer com que o aplicativo o usuário inicie um wrapper trivial (como um script em lote) que inicie o aplicativo coreclr, embora eu admita que pareça um pouco deselegante. Estou aberto a alternativas e não definido no env var. Apenas para definir as expectativas, esta não é uma área em que gastarei um esforço de design ativo em um futuro muito próximo, mas concordo que é importante ter uma configuração apropriada.

Também um aviso - supondo que continuamos no caminho de compilação em camadas de maneiras decentes, eu poderia facilmente imaginar que chegamos a um ponto em que habilitar a compilação em camadas não é apenas a inicialização mais rápida, mas também supera o desempenho atual em estado estável. No momento, o perf de inicialização é meu alvo, mas não é o limite do que podemos fazer com ele :)

Houve algum pensamento sobre criação de perfil, otimização especulativa e
desotimização?

@DemiMarie - Eles certamente surgiram em conversas e acho que muitas pessoas estão animadas que a compilação em camadas abre essas possibilidades. Falando apenas por mim, estou tentando manter o foco em fornecer os recursos básicos de compilação em camadas antes de definir minhas vistas mais altas. Outras pessoas em nossa comunidade provavelmente já estão me antecipando em outros aplicativos.

@noahfalk Sim, ser deselegante também significa que o processo usual para executá-lo pode (e muito provavelmente) tornar-se propenso a erros e esse é essencialmente o problema (a única maneira de ter certeza de que ninguém vai estragar é fazê-lo em todo o sistema). Uma alternativa que sabemos que funciona é que da mesma forma que você pode configurar se vai usar o servidor GC com uma entrada no app.config você pode fazer o mesmo com a compilação em camadas (pelo menos até o em camadas pode superar consistentemente o desempenho de estado estacionário). Sendo o JIT, você também pode fazer isso por assembly usando o assembly.config e daria um grau de recursos que atualmente não existe se outros botões puderem ser selecionados dessa maneira também.

As variáveis ​​de ambiente geralmente são definidas por usuário ou por sistema, o que tem o efeito negativo potencial de afetar todos esses processos em várias versões do tempo de execução. Um arquivo de configuração por aplicativo parece ser uma solução muito melhor (mesmo que por usuário/por sistema também esteja disponível) - algo como os valores de configuração da área de trabalho que podem ser definidos em app.config, mas também usam env vars ou registro .

Acho que devemos implementar o caminho mais comum, que é por aplicativo. As configurações de todo o sistema também podem ser úteis, mas não acho que tenhamos que pensar nisso antes que o recurso seja implementado.

Observe que não elaboramos em detalhes o que o jit de segunda camada deve fazer para otimização, embora tenhamos algumas ideias. Ele pode fazer o que o jit faz hoje, mas provavelmente fará mais.

Então deixe-me apontar algumas complicações em potencial....

É possível que o jit de segunda camada seja autoinicializado em cima das observações feitas sobre o comportamento do código criado pelo jit de primeira camada. Portanto, ignorar o jit de primeira camada e solicitar o jit de segunda camada diretamente pode não funcionar, ou pode não funcionar tão bem, como apenas deixar a camada seguir seu curso. Possivelmente, uma opção de "desvio de camadas", independentemente da implementação, acabaria fornecendo código como o código que o jit produz por padrão hoje, não o código que um jit de segunda camada poderia produzir.

O jit de segunda camada pode ser ajustado de tal forma que executá-lo em um grande conjunto de métodos cause tempos de jit relativamente lentos (já que nossa expectativa é que relativamente poucos métodos acabem sendo jitados com o jit de segunda camada, e esperamos o jit de segunda camada fará uma otimização mais completa). Ainda não sabemos as compensações certas aqui.

Dito isso...

Eu acho que um atributo de método de "otimização agressiva" faz sentido - um pedindo ao jit para se comportar um pouco como o jit de segunda camada pode se comportar para métodos específicos e talvez pular esses métodos durante o pré-jit (já que o código pré-jit é mais lento do que o código jitted, especialmente para R2R). Mas aplicar essa noção a um assembly inteiro ou a todos os assemblies em um aplicativo não parece tão atraente.

Se você tomar o que acontece em compiladores nativos como uma analogia adequada, as compensações de desempenho vs tempo de compilação/tamanho de código podem ficar muito ruins em níveis de otimização mais altos, por exemplo, compilações 10x mais longas para uma melhoria agregada de 1-2% no desempenho. A chave para o quebra-cabeça é saber quais métodos são importantes, e a única maneira de fazer isso é os programadores saberem ou o sistema descobrir por si mesmo.

@AndyAyersMS Acho que você acertou em cheio. O JIT tratando o atributo "otimização agressiva" provavelmente resolveria a maioria dos problemas de não poder ter informações suficientes para o JIT produzir no isolamento um código melhor sem que o jit de primeira camada tivesse tempo para fornecer esse feedback.

O atributo @redknightlois não funcionará se quisermos mais camadas: - T3 JIT, T4 JIT, ... Não tenho certeza se dois níveis não são suficientes, mas devemos pelo menos considerar essa possibilidade.

Seria ótimo poder usar algo semelhante ao MPGO para começar a executar com código jitted de segunda camada. Avanço rápido do primeiro nível em vez de ignorá-lo completamente.

@AndyAyersMS , o fato de a Azul ter implementado um JIT gerenciado para a JVM usando o LLVM facilitou a integração do LLVM no CLR? Aparentemente, as alterações foram enviadas para o LLVM no processo.

Apenas para informação, criei vários itens de trabalho para algum trabalho específico que precisamos fazer para sair do chão em camadas (#12609, dotnet/coreclr#12610, dotnet/coreclr#12611, dotnet/coreclr#12612, dotnet/coreclr #12617). Se o seu interesse estiver diretamente relacionado a um deles, sinta-se à vontade para adicionar seus comentários a eles. Para quaisquer outros tópicos, presumo que a discussão permaneça aqui, ou qualquer pessoa pode criar um problema para um subtópico específico se houver interesse suficiente para merecer dividi-lo por conta própria.

@MendelMonteiro Disponibilizar dados de feedback no estilo MPGO ao jitting é certamente uma opção (atualmente, só podemos ler esses dados de volta ao prejitting). Existem vários limites para o que pode ser instrumentado, então nem todos os métodos podem ser tratados dessa maneira, há outras limitações que precisamos observar (por exemplo, nenhum dado de feedback está disponível para inlinees), a instrumentação e as execuções de treinamento necessárias para criar os dados do MPGO são uma barreira para muitos usuários, e os dados do MPGO podem ou não corresponder ao que teríamos ao inicializar o primeiro nível, mas a ideia certamente tem mérito.

No que diz respeito a um nível superior baseado em LLVM - obviamente, analisamos isso até certo ponto com o LLILC e, na época, estávamos em contato frequente com o pessoal da Azul, por isso estamos familiarizados com muitas das coisas que eles estavam fazendo em LLVM para torná-lo mais acessível à compilação de linguagens com GC preciso.

Houve (e provavelmente ainda há) diferenças significativas no suporte LLVM necessário para o CLR versus o que é necessário para Java, tanto no GC quanto no EH, e nas restrições que se deve colocar no otimizador. Para citar apenas um exemplo: o CLRs GC atualmente não pode tolerar ponteiros gerenciados que apontam para o final dos objetos. Java lida com isso por meio de um mecanismo de relatório pareado base/derivado. Nós precisaríamos fornecer suporte para esse tipo de relatório emparelhado no CLR ou restringir as passagens do otimizador do LLVM para nunca criar esses tipos de ponteiros. Além disso, o jit do LLILC era lento e não tínhamos certeza de que tipo de qualidade de código ele poderia produzir.

Portanto, descobrir como o LLILC pode se encaixar em uma abordagem potencial de várias camadas que ainda não existia parecia (e ainda parece) prematuro. A ideia por enquanto é colocar camadas no framework e usar RyuJit para o jit de segunda camada. À medida que aprendemos mais, podemos descobrir que realmente há espaço para jits de nível superior ou, pelo menos, entender melhor o que mais precisamos fazer antes que essas coisas façam sentido.

@AndyAyersMS Talvez você possa introduzir as alterações necessárias no LLVM também para contornar suas limitações.

O Multicore JIT e sua otimização de perfil funcionam com o coreclr?

@benaadams - Sim, JIT multicore funciona. Não me lembro de quais (se houver) cenários em que ele está ativado por padrão, mas você pode ativá-lo via configuração: https://github.com/dotnet/coreclr/blob/master/src/inc/clrconfigvalues.h# L548

Eu escrevi um compilador de meio brinquedo e notei que na maioria das vezes as otimizações difíceis podem ser feitas razoavelmente bem na mesma infraestrutura e muito poucas coisas podem ser feitas no otimizador de nível superior.

O que quero dizer é o seguinte: se uma função for atingida muitas vezes, os parâmetros como:

  • aumentar a contagem de instruções em linha
  • use um alocador de registro mais "avançado" (colorador de retrocesso do tipo LLVM ou colorizador completo)
  • faça mais passagens de otimizações, talvez algumas especializadas com o conhecimento local. Por exemplo: permite substituir a alocação completa de objetos em alocação de pilha se o objeto for declarado no método e não for atribuído no corpo da função inline maior.
  • use PIC para a maioria dos objetos atingidos onde o CHA não é possível. Mesmo StringBuilder, por exemplo, muito provavelmente não é substituído, o código pode ser marcado como sempre que foi atingido com um StringBuilder, todos os métodos chamados dentro podem ser desvirtualizados com segurança e um protetor de tipo é definido na frente do acesso do SB.

Também seria muito bom, mas talvez este seja meu sonho acordado, que a CompilerServices ofereça o "compilador avançado" para ser exposto para poder ser acessado via código ou metadados, para que lugares como jogos ou plataformas de negociação possam se beneficiar iniciando compilação com antecedência quais classes e métodos devem ser "compilados mais profundamente". Isso não é NGen, mas se um compilador não hierárquico não for necessariamente possível (desejável), pelo menos ser possível usar o código otimizado mais pesado para partes críticas que precisam desse desempenho extra. Claro, se uma plataforma não oferece as otimizações pesadas (digamos Mono), as chamadas de API serão basicamente um NO-OP.

Temos uma base sólida para o tiering agora graças ao trabalho árduo de @noahfalk , @kouvel e outros.

Sugiro que fechemos este problema e abramos um problema "como podemos melhorar o jitting em camadas". Eu encorajo qualquer pessoa interessada no tópico a dar uma chance às camadas atuais para ter uma ideia de onde as coisas estão agora. Gostaríamos de receber feedback sobre o comportamento real, seja bom ou ruim.

O comportamento atual está descrito em algum lugar? Eu só encontrei isso , mas é mais sobre os detalhes da implementação do que especificamente sobre as camadas.

Acredito que teremos algum tipo de resumo disponível em breve, com alguns dos dados que coletamos.

O tiering pode ser habilitado na versão 2.1 definindo COMPlus_TieredCompilation=1 . Se você tentar, por favor, relate o que você encontrar ....

Com PRs recentes (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), você também pode especificar a compilação em camadas como um runtimeconfig. json ou uma propriedade de projeto msbuild. O uso dessa funcionalidade exigirá que você esteja em compilações muito recentes, enquanto a variável de ambiente já existe há algum tempo.

Como discutimos antes com @jkotas, o JIT em camadas pode melhorar o tempo de inicialização. Funciona quando usamos imagens nativas?
Fizemos medições para vários aplicativos no telefone Tizen e aí estão os resultados:

DLLs do sistema|DLLs do aplicativo|Tiered|time, s
-------|--------|------|--------
R2R |R2R |não |2,68
R2R | R2R | sim | 2,61 (-3%)
R2R |não |não |4.40
R2R | não | sim | 3,63 (-17%)

Também verificaremos o modo FNV, mas parece que funciona bem quando não há imagens.

cc @gbalykov @nkaretnikov2

Para sua informação, a compilação em camadas agora é o padrão para .NET Core: https://github.com/dotnet/coreclr/pull/19525

@alpencolt , as melhorias no tempo de inicialização podem ser menores ao usar a compilação AOT, como R2R. A melhoria do tempo de inicialização atualmente vem do jitting mais rápido com menos otimizações e, ao usar a compilação AOT, haveria menos JIT. Alguns métodos não são pré-gerados, como alguns genéricos, stubs de IL e outros métodos dinâmicos. Alguns genéricos podem se beneficiar do tiering durante a inicialização mesmo ao usar a compilação AOT.

Vou em frente fechar esta questão, pois com o commit de @kouvel acho que consegui a pergunta no título: D As pessoas são bem-vindas para continuar a discussão e/ou abrir novas questões sobre tópicos mais específicos, como melhorias solicitadas, perguntas ou investigações particulares. Se alguém achar que está fechado prematuramente, é claro que nos avise.

@kouvel Desculpe comentar sobre o assunto encerrado. Gostaria de saber ao usar a compilação AOT, como crossgen, o aplicativo ainda se beneficiará da compilação de segunda camada para os caminhos de código de ponto de acesso?

@daxian-dbw sim muito; em tempo de execução, o Jit pode fazer inlining de montagem cruzada (entre dlls); eliminação de ramificações com base em constantes de tempo de execução ( readonly static ); etc

@benaadams E um compilador AOT bem projetado não poderia?

Encontrei algumas informações sobre isso em https://blogs.msdn.microsoft.com/dotnet/2018/08/02/tiered-compilation-preview-in-net-core-2-1/ :

as imagens pré-compiladas têm restrições de versão e restrições de instrução de CPU que proíbem alguns tipos de otimização. Para quaisquer métodos nessas imagens que são chamados frequentemente de Compilação em camadas, solicita que o JIT crie código otimizado em um thread em segundo plano que substituirá a versão pré-compilada.

Sim, esse é um exemplo de "não é um AOT bem projetado". 😛

as imagens pré-compiladas têm restrições de versão e restrições de instrução de CPU que proíbem alguns tipos de otimização.

Um dos exemplos são os métodos que utilizam o hardware intrínseco. O compilador AOT (crossgen) apenas assume SSE2 como o alvo do codegen em x86/x64, então todos os métodos que usam hardware intrínseco serão rejeitados pelo crossgen e compilados pelo JIT que conhece as informações de hardware subjacentes.

E um compilador AOT bem projetado não poderia?

O compilador AOT precisa de otimização de tempo de link (para inlining de montagem cruzada) e otimização guiada por perfil (para constantes de tempo de execução). Enquanto isso, o compilador AOT precisa de informações de hardware "bottom-line" (como -mavx2 em gcc/clang) em tempo de compilação para o código SIMD.

Um dos exemplos são os métodos que utilizam o hardware intrínseco. O compilador AOT (crossgen) apenas assume SSE2 como o alvo do codegen em x86/x64, então todos os métodos que usam hardware intrínseco serão rejeitados pelo crossgen e compilados pelo JIT que conhece as informações de hardware subjacentes.

Espere o que? Não acompanho muito aqui. Por que o compilador AOT rejeitaria os intrínsecos?

E um compilador AOT bem projetado não poderia?

O compilador AOT precisa de otimização de tempo de link (para inlining de montagem cruzada) e otimização guiada por perfil (para constantes de tempo de execução). Enquanto isso, o compilador AOT precisa de informações de hardware "bottom-line" (como -mavx2 em gcc/clang) em tempo de compilação para o código SIMD.

Sim, como eu disse, "um compilador AOT bem projetado." 😁

@masonwheeler cenário diferente; crossgen é AoT que funciona com o Jit e permite a manutenção/correção de dlls sem exigir recompilação e redistribuição completa do aplicativo. Oferece melhor geração de código do que o Tier0 com inicialização mais rápida do que o Tier1; mas não é plataforma neutra.

Tier0, crossgen e Tier1 trabalham juntos como um modelo coeso em coreclr

Para fazer o cross-assembly inline (não-Jit) seria necessário a compilação de um único arquivo executável estaticamente vinculado e exigiria recompilação e redistribuição completa do aplicativo para corrigir qualquer biblioteca usada, bem como direcionar a plataforma específica (qual versão do SSE, Avx etc para usar; versão mais baixa comum ou de produção para todos?).

corert vai AoT este estilo de aplicação.

Contudo; fazer certos tipos de eliminação de ramificações que o Jit pode fazer exigiria uma grande quantidade de geração de asm extra para os caminhos alternativos; e o patch de tempo de execução da árvore correta

por exemplo, qualquer código usando um método como (onde o Tier1 Jit removerá todos os 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 fazer o cross-assembly inline (não-Jit) seria necessário a compilação de um único arquivo executável estaticamente vinculado e exigiria recompilação e redistribuição completa do aplicativo para corrigir qualquer biblioteca usada, bem como direcionar a plataforma específica (qual versão do SSE, Avx etc para usar; versão mais baixa comum ou de produção para todos?).

Não deve exigir uma redistribuição completa do aplicativo. Veja o sistema de compilação ART do Android: você distribui o aplicativo como código gerenciado (Java no caso deles, mas os mesmos princípios se aplicam) e o compilador, que fica no sistema local, AOT compila o código gerenciado em um executável nativo super-otimizado.

Se você alterar alguma pequena biblioteca, todo o código gerenciado ainda estará lá e você não precisará redistribuir tudo, apenas a coisa com o patch, e então o AOT pode ser executado novamente para produzir um novo executável. (Obviamente, é aqui que a analogia do Android falha, devido ao modelo de distribuição de aplicativos APK do Android, mas isso não se aplica ao desenvolvimento de desktop/servidor.)

e o compilador, que vive no sistema local, o AOT compila o código gerenciado...

Esse é o modelo NGen anterior que a estrutura completa usava; embora não pense que ele criou um único assembly embutido no código da estrutura no código dos aplicativos? A diferença entre duas abordagens foi destacada no Bing.com é executado no .NET Core 2.1! postagem do blog

Imagens ReadyToRun

Os aplicativos gerenciados geralmente podem ter um desempenho de inicialização ruim, pois os métodos primeiro precisam ser compilados em JIT para o código de máquina. O .NET Framework tem uma tecnologia de pré-compilação, NGEN. No entanto, o NGEN exige que a etapa de pré-compilação ocorra na máquina na qual o código será executado. Para o Bing, isso significaria NGENing em milhares de máquinas. Isso, juntamente com um ciclo de implantação agressivo, resultaria em uma redução significativa da capacidade de serviço à medida que o aplicativo é pré-compilado nas máquinas de serviço da Web. Além disso, a execução do NGEN requer privilégios administrativos, que geralmente não estão disponíveis ou são muito examinados em uma configuração de datacenter. No .NET Core, a ferramenta crossgen permite que o código seja pré-compilado como uma etapa de pré-implantação, como no laboratório de compilação, e as imagens implantadas na produção estão prontas para execução!

@masonwheeler AOT enfrenta ventos contrários em full .Net por causa da natureza dinâmica de um processo .Net. Por exemplo, corpos de métodos em .Net podem ser modificados por meio de um criador de perfil a qualquer momento, classes podem ser carregadas ou criadas por meio de reflexão, e novo código pode ser criado pelo tempo de execução conforme necessário para coisas como interoperabilidade - então, na melhor das hipóteses, as informações de análise interprocedural refletem um estado transitório no processo em execução. Qualquer análise interprocedural ou otimização (incluindo inlining) em .Net deve ser desfazível em tempo de execução.

AOT funciona melhor quando o conjunto de coisas que podem mudar entre o tempo AOT e o tempo de execução é pequeno e o impacto de tais mudanças é localizado, de modo que o escopo expansivo disponível para otimização AOT reflita amplamente coisas que sempre devem ser verdadeiras (ou talvez tenham um pequeno número de alternativas).

Se você pode criar mecanismos para lidar ou restringir a natureza dinâmica dos processos .Net, então AOT puro pode funcionar muito bem - por exemplo, .Net Native considera o impacto da reflexão e interoperabilidade e proíbe o carregamento de montagem, emissão de reflexão e ( Eu presumo) anexar perfil. Mas não é simples.

Há algum trabalho em andamento para nos permitir expandir o escopo do crossgen para vários assemblies para que possamos compilar AOT todos os frameworks principais (ou todos os assemblies asp.net) como um pacote. Mas isso só é viável porque temos o jit como substituto para refazer o codegen quando as coisas mudam.

@AndyAyersMS Eu nunca acreditei que a solução .NET AOT deveria ser uma solução "apenas AOT pura", exatamente pelos motivos que você está descrevendo aqui. Ter o JIT por perto para criar novo código conforme necessário é muito importante. Mas as situações em que é necessário são muito minoritárias e, portanto, acho que a regra de Anders Hejlsberg para sistemas de tipos pode ser aplicada de maneira lucrativa aqui:

Estático sempre que possível, dinâmico quando necessário.

De System.Linq.Expressions
public TDelegate Compile(bool preferInterpretation);

A compilação em camadas continua a funcionar se preferInterpretation for true?

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

Timovzl picture Timovzl  ·  3Comentários

nalywa picture nalywa  ·  3Comentários

v0l picture v0l  ·  3Comentários

jamesqo picture jamesqo  ·  3Comentários

matty-hall picture matty-hall  ·  3Comentários