Tornando menor o tratamento de exceções em C ++ em x64

O Visual Studio 2019 Preview 3 apresenta um novo recurso para reduzir o tamanho binário do tratamento de exceções em C ++ (tentativa / captura e destruidores automáticos) no x64. Apelidado de FH4 (para __CxxFrameHandler4, veja abaixo), desenvolvi nova formatação e processamento de dados usados ​​para tratamento de exceções em C ++ aproximadamente 60% menores que a implementação existente, resultando em redução binária geral de até 20% para programas com uso intenso de C ++ manipulação de exceção.


Este artigo no blog .

Como faço para ativar isso?


No momento, o FH4 está desativado por padrão, porque as alterações de tempo de execução necessárias para os aplicativos da Loja não puderam entrar na versão atual. Para ativar o FH4 ​​para aplicativos que não sejam da loja, passe o sinalizador não documentado “/ d2FH4” para o compilador MSVC no Visual Studio 2019 Preview 3 e além.


Planejamos ativar o FH4 ​​por padrão depois que o tempo de execução da loja for atualizado. Esperamos fazer isso no Visual Studio 2019 Update 1 e atualizaremos este post que sabemos mais.


Mudanças nas ferramentas


Qualquer instalação do Visual Studio 2019 Preview 3 e posterior terá as alterações no compilador e no tempo de execução do C ++ para dar suporte ao FH4. As alterações do compilador existem internamente sob o sinalizador "/ d2FH4" mencionado acima. O tempo de execução do C ++ possui uma nova DLL chamada vcruntime140_1.dll que é instalada automaticamente pelo VCRedist. Isso é necessário para expor o novo manipulador de exceções __CxxFrameHandler4 que substitui a rotina __CxxFrameHandler3 mais antiga. O link estático e a implantação local do aplicativo do novo tempo de execução C ++ também são suportados.


Agora para as coisas divertidas! O restante deste post abordará os resultados internos da avaliação do FH4 ​​no Windows, Office e SQL, seguidos de detalhes técnicos mais detalhados por trás dessa nova tecnologia.


Motivação e resultados


Há cerca de um ano, nossos parceiros no projeto C ++ / WinR T chegaram à equipe Microsoft C ++ com um desafio: quanto poderíamos reduzir o tamanho binário do tratamento de exceções em C ++ para programas que o utilizavam intensamente?


No contexto de um programa usando C ++ / WinRT , eles apontaram para um componente do Windows Microsoft.UI.Xaml.dll que era conhecido por possuir uma grande área binária devido ao tratamento de exceções em C ++. Confirmei que esse era realmente o caso e gerou a divisão do tamanho binário com o __CxxFrameHandler3 existente, mostrado abaixo. As porcentagens no lado direito do gráfico são porcentagens do tamanho binário total ocupado por tabelas de metadados específicas e código descrito.


Divisão de tamanho do Microsoft.UI.Xaml.dll usando __CxxFrameHandler3


Não discutirei neste post o que as estruturas específicas do lado direito do gráfico fazem (consulte a palestra de James McNellis sobre como o desenrolamento de pilha funciona no Windows para obter mais detalhes). Observando os metadados e códigos totais, no entanto, 26,4% do tamanho binário foram usados ​​pelo tratamento de exceções em C ++. Essa é uma quantidade enorme de espaço e estava dificultando a adoção do C ++ / WinRT.


Fizemos alterações no passado para reduzir o tamanho do tratamento de exceções em C ++ no compilador sem alterar o tempo de execução. Isso inclui descartar metadados para regiões de código que não podem lançar e dobrar estados logicamente idênticos. No entanto, estávamos chegando ao fim do que poderíamos fazer apenas no compilador e não conseguiríamos causar um impacto significativo em algo tão grande. A análise mostrou que havia vitórias significativas, mas exigiam alterações fundamentais nos dados, código e tempo de execução. Então fomos em frente e fizemos.


Com o novo __CxxFrameHandler4 e seus metadados, a divisão do tamanho do Microsoft.UI.XAML.dll agora é a seguinte:


Divisão de tamanho do Microsoft.UI.Xaml.dll usando __CxxFrameHandler4


O tamanho binário usado pelo tratamento de exceções em C ++ cai em 64%, levando a uma redução geral do tamanho binário de 18,6% nesse binário. Todo tipo de estrutura diminuiu de tamanho em graus surpreendentes:

Eh dataTamanho __CxxFrameHandler3 (bytes)Tamanho __CxxFrameHandler4 (bytes)% De redução de tamanho
Entradas Pdata147.864118.26020,0%
Desenrolar códigos224.28492.81058,6%
Informações da função255.44027.75589,1%
Mapas do IP2State186.94445.09875,9%
Descontrair mapas80.95269.75713,8%
Mapas do manipulador de captura52.0606.14788,2%
Experimente mapas51.9605.19690,0%
Funclets Dtor54.57045.73916,2%
Catch funclets102.4004,30195,8%
Total1.156.474415.06364,1%

Em conjunto, a mudança para __CxxFrameHandler4 reduziu o tamanho geral do Microsoft.UI.Xaml.dll de 4,4 MB para 3,6 MB.


A avaliação do FH4 ​​em um conjunto representativo de binários do Office mostra uma redução de tamanho de ~ 10% nas DLLs que usam muito as exceções. Mesmo no Word e no Excel, projetados para minimizar o uso de exceções, ainda há uma redução significativa no tamanho binário.

BinárioTamanho antigo (MB)Novo tamanho (MB)% De redução de tamanhoDescrição do produto
chart.dll17,2715.1012,6%Suporte para interagir com tabelas e gráficos
Csi.dll9,788.6611,4%Suporte para trabalhar com arquivos armazenados na nuvem
Mso20Win32Client.dll6.075,4111,0%Código comum compartilhado entre todos os aplicativos do Office
Mso30Win32Client.dll8,117,309,9%Código comum compartilhado entre todos os aplicativos do Office
oart.dll18,2116,2011,0%Recursos gráficos compartilhados entre aplicativos do Office
wwlib.dll42,1541.122,5%O principal binário do Microsoft Word
excel.exe52,8650,294,9%O principal binário do Microsoft Excel

A avaliação do FH4 ​​nos binários principais do SQL mostra uma redução de 4-21% no tamanho, principalmente da compactação de metadados descrita na próxima seção:

BinárioTamanho antigo (MB)Novo tamanho (MB)% De redução de tamanhoDescrição do produto
sqllang.dll47.1244,335,9%Serviços de nível superior: analisador de idiomas, fichário, otimizador e mecanismo de execução
sqlmin.dll48,1745,834,8%Serviços de baixo nível: transações e mecanismo de armazenamento
qds.dll1,421,336,3%Funcionalidade de armazenamento de consulta
SqlDK.dll3,193.054,4%Abstrações do SQL OS: memória, threads, agendamento, etc.
autoadmin.dll1,771,647,3%Lógica do orientador de ajuste do banco de dados
xedetours.dll0,450,3621,6%Gravador de dados de voo para consultas

A tecnologia


Ao analisar o que fez com que a exceção do C ++ manipulasse os dados fosse tão grande no Microsoft.UI.Xaml.dll, encontrei dois culpados principais:


  1. As estruturas de dados são grandes: as tabelas de metadados eram de tamanho fixo, com campos de deslocamentos relativos à imagem e números inteiros com quatro bytes de comprimento. Uma função com uma única tentativa / captura e um ou dois destruidores automáticos tinham mais de 100 bytes de metadados.
  2. As estruturas de dados e o código gerado não eram passíveis de mesclagem. As tabelas de metadados continham deslocamentos relativos à imagem que impediam o dobramento COMDAT (o processo em que o vinculador pode dobrar pedaços de dados idênticos para economizar espaço), a menos que as funções que eles representavam sejam idênticas. Além disso, os funclets de captura (código descrito nos blocos de captura do programa) não podiam ser dobrados, mesmo se fossem idênticos ao código, porque seus metadados estão contidos nos pais.

Para resolver esses problemas, o FH4 ​​reestrutura os metadados e o código de forma que:


  1. Os valores de tamanho fixo anteriores foram compactados usando uma codificação de número inteiro de comprimento variável que reduz> 90% dos campos de metadados de quatro bytes para um. As tabelas de metadados agora também têm comprimento variável com um cabeçalho para indicar se determinados campos estão presentes para economizar espaço na emissão de campos vazios.
  2. Todos os deslocamentos relativos à imagem que podem ser relativos à função foram feitos relativos à função. Isso permite dobrar COMDAT entre metadados de diferentes funções com características semelhantes (instanciações do modelo de pensamento) e permite que esses valores sejam compactados. Os funclets de captura foram reprojetados para deixar de ter seus metadados armazenados nos pais, de modo que agora os funclets de captura idênticos ao código podem ser dobrados em uma única cópia no binário.

Para ilustrar isso, vejamos a definição original da tabela de metadados de informações da função usada para __CxxFrameHandler3. Esta é a tabela inicial para o tempo de execução ao processar o EH e aponta para as outras tabelas de metadados. Esse código está disponível publicamente em qualquer instalação do VS, procure <caminho da instalação do VS> \ VC \ Tools \ MSVC \ <versão> \ include \ ehdata.h:


typedef const struct _s_FuncInfo { unsigned int magicNumber:29; // Identifies version of compiler unsigned int bbtFlags:3; // flags that may be set by BBT processing __ehstate_t maxState; // Highest state number plus one (thus // number of entries in unwind map) int dispUnwindMap; // Image relative offset of the unwind map unsigned int nTryBlocks; // Number of 'try' blocks in this function int dispTryBlockMap; // Image relative offset of the handler map unsigned int nIPMapEntries; // # entries in the IP-to-state map. NYI (reserved) int dispIPtoStateMap; // Image relative offset of the IP to state map int dispUwindHelp; // Displacement of unwind helpers from base int dispESTypeList; // Image relative list of types for exception specifications int EHFlags; // Flags for some features. } FuncInfo; 

Essa estrutura é de tamanho fixo, contendo 10 campos a cada 4 bytes. Isso significa que todas as funções que precisam de tratamento de exceção C ++, por padrão, incorrem em 40 bytes de metadados.


Agora, para a nova estrutura de dados (<caminho de instalação do VS> \ VC \ Tools \ MSVC \ <versão> \ include \ ehdata4_export.h):


 struct FuncInfoHeader { union { struct { uint8_t isCatch : 1; // 1 if this represents a catch funclet, 0 otherwise uint8_t isSeparated : 1; // 1 if this function has separated code segments, 0 otherwise uint8_t BBT : 1; // Flags set by Basic Block Transformations uint8_t UnwindMap : 1; // Existence of Unwind Map RVA uint8_t TryBlockMap : 1; // Existence of Try Block Map RVA uint8_t EHs : 1; // EHs flag set uint8_t NoExcept : 1; // NoExcept flag set uint8_t reserved : 1; }; uint8_t value; }; }; struct FuncInfo4 { FuncInfoHeader header; uint32_t bbtFlags; // flags that may be set by BBT processing int32_t dispUnwindMap; // Image relative offset of the unwind map int32_t dispTryBlockMap; // Image relative offset of the handler map int32_t dispIPtoStateMap; // Image relative offset of the IP to state map uint32_t dispFrame; // displacement of address of function frame wrt establisher frame, only used for catch funclets }; 

Observe que:


  1. O número mágico foi removido, emitindo 0x19930522 toda vez que se torna um problema quando um programa possui milhares dessas entradas.
  2. O EHFlags foi movido para o cabeçalho enquanto dispESTypeList foi eliminado devido ao suporte abandonado das especificações de exceção dinâmica no C ++ 17. O compilador usará como padrão o __CxxFrameHandler3 mais antigo se especificações de exceção dinâmica forem usadas.
  3. Os comprimentos das outras tabelas não são mais armazenados em "Informações da Função 4". Isso permite que a dobragem COMDAT dobre mais tabelas apontadas, mesmo que a tabela “Function Info 4” não possa ser dobrada.
  4. (Não mostrado explicitamente) Os campos dispFrame e bbtFlags agora são números inteiros de comprimento variável. A representação de alto nível o deixa como um uint32_t para facilitar o processamento.
  5. bbtFlags, dispUnwindMap, dispTryBlockMap e dispFrame podem ser omitidos, dependendo dos campos definidos no cabeçalho.

Levando tudo isso em consideração, o tamanho médio da nova estrutura “Function Info 4” agora é de 13 bytes (cabeçalho de 1 byte + três deslocamentos relativos da imagem de 4 bytes em relação a outras tabelas) que podem ser reduzidos ainda mais se algumas tabelas não forem necessárias. Os tamanhos das tabelas foram removidos, mas agora esses valores são compactados e 90% deles no Microsoft.UI.Xaml.dll foram encontrados para caber em um único byte. Juntando tudo isso, isso significa que o tamanho médio para representar os mesmos dados funcionais no novo manipulador é de 16 bytes em comparação com os 40 bytes anteriores - uma melhoria bastante dramática!


Para dobrar, vejamos o número de tabelas e funclets exclusivos com o manipulador antigo e novo:

Eh dataContagem em __CxxFrameHandler3Contagem em __CxxFrameHandler4% De redução
Entradas Pdata12.3229.85520,0%
Informações da função6.3862.74757,0%
Entradas do Mapa IP2State6.3632.14866,2%
Descontrair entradas do mapa1.4871.4641,5%
Mapas do manipulador de captura2.60360176,9%
Experimente mapas2.59864875,1%
Funclets Dtor2,3011.52733,6%
Catch funclets2.6038496,8%
Total36.66319.07448,0%

O número de entradas de dados EH exclusivas diminui em 48% ao criar oportunidades adicionais de dobragem, removendo RVAs e redesenhando os funclets de captura. Quero especificamente chamar o número de funclets de captura em itálico em verde: ele cai de 2.603 para apenas 84. Isso é uma consequência da conversão de HRESULTs em C ++ / WinRT para exceções de C ++, que gera muitos funclets de captura idênticos ao código que agora podem ser dobrado. Certamente, uma queda dessa magnitude está no alto nível dos resultados, mas demonstra o potencial de economia de tamanho que a dobragem pode alcançar quando as estruturas de dados são projetadas com isso em mente.


Desempenho


Com o design introduzindo a compactação e modificando a execução do tempo de execução, houve a preocupação de causar impacto no desempenho do tratamento de exceções. O impacto, no entanto, é positivo : o desempenho do tratamento de exceções melhora com __CxxFrameHandler4 em oposição a __CxxFrameHandler3. Testei o rendimento usando um programa de benchmark que se desdobra em 100 quadros de pilha, cada um com um try / catch e 3 objetos automáticos para destruir. Isso foi executado 50.000 vezes para perfilar o tempo de execução, levando a tempos gerais de execução de:

__CxxFrameHandler3__CxxFrameHandler4
Tempo de execução4.84s4.25s

A criação de perfil mostrou que a descompactação introduz tempo de processamento adicional, mas seu custo é superado por menos lojas para armazenar armazenamento local no novo design de tempo de execução.


Planos futuros


Conforme mencionado no título, o FH4 ​​atualmente está ativado apenas para binários x64. No entanto, as técnicas descritas são extensíveis para ARM32 / ARM64 e, em menor grau, x86. No momento, estamos procurando bons exemplos (como Microsoft.UI.Xaml.dll) para motivar a extensão dessa tecnologia para outras plataformas - se você acha que tem um bom caso de uso, avise-nos!


O processo de integração das alterações de tempo de execução dos aplicativos da loja para suportar o FH4 ​​está em andamento. Feito isso, o novo manipulador será ativado por padrão para que todos possam obter essas economias de tamanho binário sem nenhum esforço adicional.


Considerações finais


Para quem acha que seus binários x64 poderiam reduzir um pouco: experimente o FH4 ​​(via '/ d2FH4') hoje! Estamos empolgados em ver que economia isso pode proporcionar agora que esse recurso está disponível. Obviamente, se você encontrar algum problema, informe-nos nos comentários abaixo, por e-mail ( visualcpp@microsoft.com ) ou através da Comunidade do desenvolvedor . Você também pode encontrar-nos no Twitter ( @VisualC ).


Agradecemos a Kenny Kerr por nos direcionar para Microsoft.UI.Xaml.dll, Ravi Pinjala por reunir os números no Office e Robert Roessler por testar isso no SQL.

Source: https://habr.com/ru/post/pt442996/


All Articles