
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.

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:

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 data | Tamanho __CxxFrameHandler3 (bytes) | Tamanho __CxxFrameHandler4 (bytes) | % De redução de tamanho |
Entradas Pdata | 147.864 | 118.260 | 20,0% |
Desenrolar códigos | 224.284 | 92.810 | 58,6% |
Informações da função | 255.440 | 27.755 | 89,1% |
Mapas do IP2State | 186.944 | 45.098 | 75,9% |
Descontrair mapas | 80.952 | 69.757 | 13,8% |
Mapas do manipulador de captura | 52.060 | 6.147 | 88,2% |
Experimente mapas | 51.960 | 5.196 | 90,0% |
Funclets Dtor | 54.570 | 45.739 | 16,2% |
Catch funclets | 102.400 | 4,301 | 95,8% |
Total | 1.156.474 | 415.063 | 64,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ário | Tamanho antigo (MB) | Novo tamanho (MB) | % De redução de tamanho | Descrição do produto |
chart.dll | 17,27 | 15.10 | 12,6% | Suporte para interagir com tabelas e gráficos |
Csi.dll | 9,78 | 8.66 | 11,4% | Suporte para trabalhar com arquivos armazenados na nuvem |
Mso20Win32Client.dll | 6.07 | 5,41 | 11,0% | Código comum compartilhado entre todos os aplicativos do Office |
Mso30Win32Client.dll | 8,11 | 7,30 | 9,9% | Código comum compartilhado entre todos os aplicativos do Office |
oart.dll | 18,21 | 16,20 | 11,0% | Recursos gráficos compartilhados entre aplicativos do Office |
wwlib.dll | 42,15 | 41.12 | 2,5% | O principal binário do Microsoft Word |
excel.exe | 52,86 | 50,29 | 4,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ário | Tamanho antigo (MB) | Novo tamanho (MB) | % De redução de tamanho | Descrição do produto |
sqllang.dll | 47.12 | 44,33 | 5,9% | Serviços de nível superior: analisador de idiomas, fichário, otimizador e mecanismo de execução |
sqlmin.dll | 48,17 | 45,83 | 4,8% | Serviços de baixo nível: transações e mecanismo de armazenamento |
qds.dll | 1,42 | 1,33 | 6,3% | Funcionalidade de armazenamento de consulta |
SqlDK.dll | 3,19 | 3.05 | 4,4% | Abstrações do SQL OS: memória, threads, agendamento, etc. |
autoadmin.dll | 1,77 | 1,64 | 7,3% | Lógica do orientador de ajuste do banco de dados |
xedetours.dll | 0,45 | 0,36 | 21,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:
- 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.
- 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:
- 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.
- 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;
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;
Observe que:
- O número mágico foi removido, emitindo 0x19930522 toda vez que se torna um problema quando um programa possui milhares dessas entradas.
- 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.
- 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.
- (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.
- 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 data | Contagem em __CxxFrameHandler3 | Contagem em __CxxFrameHandler4 | % De redução |
Entradas Pdata | 12.322 | 9.855 | 20,0% |
Informações da função | 6.386 | 2.747 | 57,0% |
Entradas do Mapa IP2State | 6.363 | 2.148 | 66,2% |
Descontrair entradas do mapa | 1.487 | 1.464 | 1,5% |
Mapas do manipulador de captura | 2.603 | 601 | 76,9% |
Experimente mapas | 2.598 | 648 | 75,1% |
Funclets Dtor | 2,301 | 1.527 | 33,6% |
Catch funclets | 2.603 | 84 | 96,8% |
Total | 36.663 | 19.074 | 48,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ção | 4.84s | 4.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.