"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Aprimorando o desempenho com análise estática, inicialização de imagem e captura de heap

Aprimorando o desempenho com análise estática, inicialização de imagem e captura de heap

Publicado em 2024-11-08
Navegar:894

Das estruturas monolíticas ao mundo dos sistemas distribuídos, o desenvolvimento de aplicativos já percorreu um longo caminho. A adoção massiva da computação em nuvem e da arquitetura de microsserviços alterou significativamente a abordagem de como os aplicativos de servidor são criados e implantados. Em vez de servidores de aplicativos gigantes, agora temos serviços independentes e implantados individualmente que entram em ação
como e quando necessário.

No entanto, um novo player no bloco que pode impactar esse bom funcionamento pode ser 'inicializações a frio'. As partidas a frio entram em ação quando a primeira solicitação é processada em um trabalhador recém-gerado. Esta situação exige a inicialização do tempo de execução da linguagem e a inicialização da configuração do serviço antes de processar a solicitação real. A imprevisibilidade e a execução mais lenta associadas às partidas a frio podem violar os acordos de nível de serviço de um serviço em nuvem. Então, como combater essa preocupação crescente?

Imagem nativa: otimizando o tempo de inicialização e o consumo de memória

Para combater as ineficiências das partidas a frio, uma nova abordagem foi desenvolvida envolvendo análise de pontos, inicialização do aplicativo em tempo de construção, instantâneo de heap e compilação (AOT) antecipada. Este método opera sob uma suposição de mundo fechado, exigindo que todas as classes Java sejam predeterminadas e acessíveis no momento da construção. Durante esta fase, uma análise abrangente de pontos determina todos os elementos acessíveis do programa (classes, métodos, campos) para garantir que apenas métodos Java essenciais sejam compilados.

O código de inicialização do aplicativo pode ser executado durante o processo de construção, e não em tempo de execução. Isto permite a pré-alocação de objetos Java e a construção de estruturas de dados complexas, que são então disponibilizadas em tempo de execução através de um "heap de imagens". Esse heap de imagens é integrado ao executável, proporcionando disponibilidade imediata no início do aplicativo. O
a execução iterativa de análise de pontos a ponto e captura de instantâneos continua até que um estado estável (ponto fixo) seja alcançado, otimizando o tempo de inicialização e o consumo de recursos.

Fluxo de trabalho detalhado

A entrada para nosso sistema é o bytecode Java, que pode se originar de linguagens como Java, Scala ou Kotlin. O processo trata o aplicativo, suas bibliotecas, o JDK e os componentes da VM de maneira uniforme para produzir um executável nativo específico para um sistema operacional e arquitetura - denominado "imagem nativa". O processo de construção inclui análise iterativa de pontos e instantâneos de heap até que um ponto fixo seja alcançado, permitindo que o aplicativo participe ativamente por meio de retornos de chamada registrados. Essas etapas são conhecidas coletivamente como processo de construção de imagem nativa (Figura 1)

Enhancing Performance with Static Analysis, Image Initialization and Heap Snapshotting

Figura 1 – Processo de construção de imagem nativa (fonte: redhat.com)

Análise de pontos

Empregamos uma análise de pontos para verificar a acessibilidade de classes, métodos e campos durante o tempo de execução. A análise de pontos começa com todos os pontos de entrada, como o método principal da aplicação, e percorre iterativamente todos os métodos transitivamente alcançáveis ​​até chegar a um ponto fixo(Figura 2).

Enhancing Performance with Static Analysis, Image Initialization and Heap Snapshotting

Figura 2 – Pontos para análise

Nossa análise de pontos aproveita o front-end do nosso compilador para analisar o bytecode Java na representação intermediária de alto nível do compilador (IR). Posteriormente, o IR é transformado em um gráfico de fluxo de tipo. Neste gráfico, os nós representam instruções operando em tipos de objetos, enquanto as arestas denotam arestas de uso direcionado entre os nós, apontando da definição para o uso. Cada nó mantém um estado de tipo, que consiste em uma lista de tipos que podem alcançar o nó e informações de nulidade. Os estados de tipo se propagam pelas arestas de uso; se o estado do tipo de um nó for alterado, essa alteração será disseminada para todos os usos. É importante ressaltar que os estados de tipo só podem ser expandidos; novos tipos podem ser adicionados a um estado de tipo, mas os tipos existentes nunca são removidos. Este mecanismo garante que o
a análise finalmente converge para um ponto fixo, levando à terminação.

Execute o código de inicialização

A análise de pontos orienta a execução do código de inicialização quando ele atinge um ponto fixo local. Este código tem origem em duas fontes distintas: inicializadores de classe e lote de código personalizado executado em tempo de construção por meio de uma interface de recurso:

  1. Inicializadores de classe: Toda classe Java pode ter um inicializador de classe indicado por um método , que inicializa campos estáticos. Os desenvolvedores podem escolher quais classes inicializar em tempo de construção versus tempo de execução.

  2. Retornos de chamada explícitos: Os desenvolvedores podem implementar código personalizado por meio de ganchos fornecidos pelo nosso sistema, executando antes, durante ou depois dos estágios de análise.

Aqui estão as APIs fornecidas para integração com nosso sistema.

API passiva (consulta o status atual da análise)

boolean isReachable(Class> clazz);

boolean isReachable(Field field);

boolean isReachable(Executable method);

Para obter mais informações, consulte QueryReachabilityAccess

API ativa (registra retornos de chamada para alterações de status de análise):

void registerReachabilityHandler(Consumer callback, Object... elements);

void registerSubtypeReachabilityHandler(BiConsumer> callback, Class> baseClass);

void registerMethodOverrideReachabilityHandler(BiConsumer callback, Executable baseMethod);

Para obter mais informações, consulte BeforeAnalysisAccess

Durante esta fase, o aplicativo pode executar código personalizado, como alocação de objetos e inicialização de estruturas de dados maiores. É importante ressaltar que o código de inicialização pode acessar o estado atual de análise de pontos, permitindo consultas sobre a acessibilidade de tipos, métodos ou campos. Isso é feito usando os vários métodos isReachable() fornecidos por WhileAnalysisAccess. Aproveitando essas informações, o aplicativo pode construir estruturas de dados otimizadas para os segmentos acessíveis do aplicativo.

Instantâneo de pilha

Finalmente, o heap snapshot constrói um gráfico de objetos seguindo ponteiros raiz como campos estáticos para construir uma visão abrangente de todos os objetos acessíveis. Este gráfico então preenche o
da imagem nativa heap de imagem, garantindo que o estado inicial do aplicativo seja carregado com eficiência na inicialização.

Para gerar o fechamento transitivo de objetos alcançáveis, o algoritmo percorre campos de objetos, lendo seus valores por meio de reflexão. É crucial observar que o construtor de imagens opera no ambiente Java. Somente os campos de instância marcados como "lidos" pela análise de pontos são considerados durante esta travessia. Por exemplo, se uma classe tiver dois campos de instância, mas um não estiver marcado como lido, o objeto acessível por meio do campo não marcado será excluído do heap de imagem.

Ao encontrar um valor de campo cuja classe não tenha sido previamente identificada pela análise de pontos, a classe é registrada como um tipo de campo. Este registro garante que em iterações subsequentes da análise de pontos, o novo tipo seja propagado para todas as leituras de campo e usos transitivos no gráfico de fluxo de tipo.

O trecho de código abaixo descreve o algoritmo principal para instantâneo de heap:

Declare List worklist := []
Declare Set reachableObjects := []

Function BuildHeapSnapshot(PointsToState pointsToState)
For Each field in pointsToState.getReachableStaticObjectFields()
Call AddObjectToWorkList(field.readValue())
End For

    For Each method in pointsToState.getReachableMethods()
        For Each constant in method.embeddedConstants()
            Call AddObjectToWorkList(constant)
        End For
    End For

    While worklist.isNotEmpty
        Object current := Pop from worklist
        If current Object is an Array
            For Each value in current
                Call AddObjectToWorkList(value)
                Add current.getClass() to pointsToState.getObjectArrayTypes()
            End For
        Else
            For Each field in pointsToState.getReachableInstanceObjectFields(current.getClass())
                Object value := field.read(current)
                Call AddObjectToWorkList(value)
                Add value.getClass() to pointsToState.getFieldValueTypes(field)
            End For
        End If
    End While
    Return reachableObjects
End Function

Em resumo, o algoritmo de captura instantânea de heap constrói com eficiência um instantâneo do heap percorrendo sistematicamente objetos acessíveis e seus campos. Isso garante que apenas objetos relevantes sejam incluídos no heap de imagem, otimizando o desempenho e o consumo de memória da imagem nativa.

Conclusão

Concluindo, o processo de heap snapshot desempenha um papel crítico na criação de imagens nativas. Ao percorrer sistematicamente objetos alcançáveis ​​e seus campos, o algoritmo de instantâneo de heap constrói um gráfico de objetos que representa o fechamento transitivo de objetos alcançáveis ​​a partir de ponteiros raiz, como campos estáticos. Este gráfico de objeto é então incorporado à imagem nativa como o heap de imagens, servindo como o heap inicial na inicialização da imagem nativa.

Ao longo do processo, o algoritmo depende do estado da análise de pontos para determinar quais objetos e campos são relevantes para inclusão na pilha de imagens. São considerados objetos e campos marcados como “lidos” pela análise de pontos, enquanto são excluídas entidades não marcadas. Além disso, ao encontrar tipos inéditos, o algoritmo os registra para propagação em iterações subsequentes da análise de pontos.

No geral, o heap snapshot otimiza o desempenho e o uso de memória de imagens nativas, garantindo que apenas os objetos necessários sejam incluídos no heap de imagem. Essa abordagem sistemática aumenta a eficiência e a confiabilidade da execução de imagens nativas.

Declaração de lançamento Este artigo foi reproduzido em: https://dev.to/yanev/enhancing-performance-with-static-análise-image-initialization-and-heap-snapshotting-263f?1 Se houver alguma violação, entre em contato com study_golang@163 .com para excluí-lo
Tutorial mais recente Mais>

Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.

Copyright© 2022 湘ICP备2022001581号-3