
Visual Studio 2019 Preview 3 presenta una nueva característica para reducir el tamaño binario del manejo de excepciones de C ++ (try / catch y destructores automáticos) en x64. Apodado FH4 (para __CxxFrameHandler4, ver más abajo), desarrollé un nuevo formato y procesamiento para los datos utilizados para el manejo de excepciones de C ++ que es ~ 60% más pequeño que la implementación existente, lo que resulta en una reducción binaria general de hasta un 20% para programas con un uso intensivo de C ++ manejo de excepciones.
Este artículo en el
blog .
¿Cómo enciendo esto?
FH4 está actualmente desactivado de manera predeterminada porque los cambios de tiempo de ejecución necesarios para las aplicaciones de la Tienda no pudieron ingresar a la versión actual. Para activar FH4 para aplicaciones que no son de la Tienda, pase el indicador indocumentado "/ d2FH4" al compilador MSVC en Visual Studio 2019 Preview 3 y más allá.
Planeamos habilitar FH4 de forma predeterminada una vez que se haya actualizado el tiempo de ejecución de la Tienda. Esperamos hacer esto en la Actualización 1 de Visual Studio 2019 y actualizaremos esta publicación una que sepamos más.
Cambios de herramientas
Cualquier instalación de Visual Studio 2019 Preview 3 y posteriores tendrá los cambios en el compilador y el tiempo de ejecución de C ++ para admitir FH4. Los cambios del compilador existen internamente bajo el indicador "/ d2FH4" mencionado anteriormente. El tiempo de ejecución de C ++ tiene una nueva DLL llamada vcruntime140_1.dll que VCRedist instala automáticamente. Esto es necesario para exponer el nuevo controlador de excepciones __CxxFrameHandler4 que reemplaza la rutina anterior __CxxFrameHandler3. La vinculación estática y la implementación local de la aplicación del nuevo tiempo de ejecución de C ++ también son compatibles.
Ahora a las cosas divertidas! El resto de esta publicación cubrirá los resultados internos de probar FH4 en Windows, Office y SQL, seguido de detalles técnicos más detallados detrás de esta nueva tecnología.
Motivación y resultados.
Hace aproximadamente un año, nuestros socios en el proyecto C ++ / WinR T llegaron al equipo de Microsoft C ++ con un desafío: ¿cuánto podríamos reducir el tamaño binario del manejo de excepciones de C ++ para programas que lo usaron en gran medida?
En el contexto de un programa que usa C ++ / WinRT , nos señalaron a un componente de Windows Microsoft.UI.Xaml.dll que se sabía que tenía una gran huella binaria debido al manejo de excepciones de C ++. Confirmé que este era realmente el caso y generé el desglose del tamaño binario con el __CxxFrameHandler3 existente, que se muestra a continuación. Los porcentajes en el lado derecho del gráfico son el porcentaje del tamaño binario total ocupado por tablas de metadatos específicos y código resumido.

No discutiré en esta publicación qué hacen las estructuras específicas en el lado derecho del gráfico (vea la charla de James McNellis sobre cómo funciona el desbobinado de la pila en Windows para más detalles). Sin embargo, al observar los metadatos y el código totales, el manejo de excepciones de C ++ utilizó un enorme 26.4% del tamaño binario. Esta es una cantidad enorme de espacio y estaba obstaculizando la adopción de C ++ / WinRT.
Hemos realizado cambios en el pasado para reducir el tamaño del manejo de excepciones de C ++ en el compilador sin cambiar el tiempo de ejecución. Esto incluye eliminar metadatos para regiones de código que no pueden arrojar y plegar estados lógicamente idénticos. Sin embargo, estábamos llegando al final de lo que podíamos hacer solo en el compilador y no podríamos hacer una mella significativa en algo tan grande. El análisis mostró que había importantes victorias, pero que requerían cambios fundamentales en los datos, el código y el tiempo de ejecución. Así que seguimos adelante y los hicimos.
Con el nuevo __CxxFrameHandler4 y los metadatos que lo acompañan, el desglose de tamaño para Microsoft.UI.XAML.dll ahora es el siguiente:

El tamaño binario utilizado por el manejo de excepciones de C ++ disminuye en un 64%, lo que lleva a una disminución general del tamaño binario del 18,6% en este binario. Cada tipo de estructura se redujo en tamaño por grados asombrosos:
Eh datos | __CxxFrameHandler3 Tamaño (Bytes) | __CxxFrameHandler4 Tamaño (Bytes) | % De reducción de tamaño |
Entradas de datos | 147,864 | 118,260 | 20,0% |
Desenrollar códigos | 224,284 | 92,810 | 58,6% |
Información de funciones | 255,440 | 27,755 | 89,1% |
Mapas de estado IP2 | 186,944 | 45,098 | 75,9% |
Relájese mapas | 80,952 | 69,757 | 13,8% |
Capturar mapas de manejador | 52,060 | 6,147 | 88,2% |
Prueba mapas | 51,960 | 5,196 | 90,0% |
Dcle funclets | 54,570 | 45,739 | 16,2% |
Atrapar funclets | 102,400 | 4,301 | 95,8% |
Total | 1,156,474 | 415,063 | 64,1% |
Combinado, el cambio a __CxxFrameHandler4 redujo el tamaño total de Microsoft.UI.Xaml.dll de 4.4 MB a 3.6 MB.
Probar FH4 en un conjunto representativo de binarios de Office muestra una reducción de tamaño de ~ 10% en las DLL que usan muchas excepciones. Incluso en Word y Excel, que están diseñados para minimizar el uso de excepciones, todavía hay una reducción significativa en el tamaño binario.
Binario | Tamaño antiguo (MB) | Nuevo tamaño (MB) | % De reducción de tamaño | Descripción |
chart.dll | 17,27 | 15.10 | 12,6% | Soporte para interactuar con tablas y gráficos. |
Csi.dll | 9,78 | 8.66 | 11,4% | Soporte para trabajar con archivos almacenados en la nube |
Mso20Win32Client.dll | 6.07 | 5.41 | 11,0% | Código común que se comparte entre todas las aplicaciones de Office |
Mso30Win32Client.dll | 8.11 | 7.30 | 9,9% | Código común que se comparte entre todas las aplicaciones de Office |
oart.dll | 18,21 | 16,20 | 11,0% | Funciones gráficas que se comparten entre las aplicaciones de Office |
wwlib.dll | 42,15 | 41,12 | 2.5% | Binario principal de Microsoft Word |
excel.exe | 52,86 | 50,29 | 4.9% | Binario principal de Microsoft Excel |
La prueba de FH4 en los archivos binarios SQL centrales muestra una reducción del tamaño del 4-21%, principalmente a partir de la compresión de metadatos que se describe en la siguiente sección:
Binario | Tamaño antiguo (MB) | Nuevo tamaño (MB) | % De reducción de tamaño | Descripción |
sqllang.dll | 47,12 | 44,33 | 5,9% | Servicios de nivel superior: analizador de lenguaje, carpeta, optimizador y motor de ejecución |
sqlmin.dll | 48,17 | 45,83 | 4.8% | Servicios de bajo nivel: motor de transacciones y almacenamiento |
qds.dll | 1,42 | 1,33 | 6.3% | Consultar la funcionalidad de la tienda |
SqlDK.dll | 3.19 | 3,05 | 4.4% | Abstracciones del sistema operativo SQL: memoria, subprocesos, programación, etc. |
autoadmin.dll | 1,77 | 1,64 | 7.3% | Lógica del asesor de ajuste de base de datos |
xedetours.dll | 0,45 | 0,36 | 21,6% | Registrador de datos de vuelo para consultas |
La tecnología
Al analizar qué causó que los datos de manejo de excepciones de C ++ fueran tan grandes en Microsoft.UI.Xaml.dll, encontré dos culpables principales:
- Las estructuras de datos en sí mismas son grandes: las tablas de metadatos tenían un tamaño fijo con campos de desplazamientos relativos a la imagen y enteros de cuatro bytes de longitud. Una función con un solo intento / captura y uno o dos destructores automáticos tenía más de 100 bytes de metadatos.
- Las estructuras de datos y el código generado no eran susceptibles de fusión. Las tablas de metadatos contenían desplazamientos relativos a la imagen que impedían el plegado de COMDAT (el proceso en el que el enlazador puede plegar piezas de datos idénticas para ahorrar espacio) a menos que las funciones que representaban fueran idénticas. Además, los funclets de captura (código resumido de los bloques de captura del programa) no se podrían plegar incluso si fueran idénticos al código porque sus metadatos están contenidos en sus padres.
Para abordar estos problemas, FH4 reestructura los metadatos y el código de manera que:
- Los valores de tamaño fijo anteriores se han comprimido utilizando una codificación entera de longitud variable que reduce> 90% de los campos de metadatos de cuatro bytes a uno. Las tablas de metadatos ahora también tienen una longitud variable con un encabezado para indicar si ciertos campos están presentes para ahorrar espacio al emitir campos vacíos.
- Todos los desplazamientos relativos a la imagen que pueden ser relativos a la función se han hecho relativos a la función. Esto permite que COMDAT se pliegue entre metadatos de diferentes funciones con características similares (piense en instancias de plantillas) y permite comprimir estos valores. Los funclets de captura se han rediseñado para que ya no tengan sus metadatos almacenados en sus padres, de modo que cualquier funclets de captura idéntico al código ahora se pueda plegar en una sola copia en el binario.
Para ilustrar esto, echemos un vistazo a la definición original de la tabla de metadatos de Información de función utilizada para __CxxFrameHandler3. Esta es la tabla de inicio para el tiempo de ejecución al procesar EH y apunta a las otras tablas de metadatos. Este código está disponible públicamente en cualquier instalación de VS, busque <Ruta de instalación de VS> \ VC \ Tools \ MSVC \ <versión> \ include \ ehdata.h:
typedef const struct _s_FuncInfo { unsigned int magicNumber:29;
Esta estructura tiene un tamaño fijo que contiene 10 campos cada uno de 4 bytes de longitud. Esto significa que cada función que necesita el manejo de excepciones de C ++ por defecto incurre en 40 bytes de metadatos.
Ahora a la nueva estructura de datos (<Ruta de instalación VS> \ VC \ Tools \ MSVC \ <versión> \ include \ ehdata4_export.h):
struct FuncInfoHeader { union { struct { uint8_t isCatch : 1;
Tenga en cuenta que:
- El número mágico se ha eliminado, emitir 0x19930522 cada vez que se convierte en un problema cuando un programa tiene miles de estas entradas.
- EHFlags se ha movido al encabezado mientras que dispESTypeList se ha eliminado debido a la caída del soporte de las especificaciones de excepción dinámica en C ++ 17. El compilador usará de forma predeterminada el __CxxFrameHandler3 más antiguo si se utilizan especificaciones de excepción dinámica.
- Las longitudes de las otras tablas ya no se almacenan en "Información de la función 4". Esto permite que el plegado COMDAT pliegue más tablas puntiagudas incluso si la tabla "Información de función 4" en sí no se puede plegar.
- (No se muestra explícitamente) Los campos dispFrame y bbtFlags ahora son enteros de longitud variable. La representación de alto nivel lo deja como un uint32_t para un procesamiento fácil.
- bbtFlags, dispUnwindMap, disp CalculateBlockMap y dispFrame pueden omitirse según los campos establecidos en el encabezado.
Teniendo todo esto en cuenta, el tamaño promedio de la nueva estructura de "Información de función 4" ahora es de 13 bytes (encabezado de 1 byte + tres desplazamientos relativos de imagen de 4 bytes con respecto a otras tablas) que pueden reducirse aún más si no se necesitan algunas tablas. Las longitudes de las tablas se movieron, pero estos valores ahora están comprimidos y se encontró que el 90% de ellos en Microsoft.UI.Xaml.dll cabía dentro de un solo byte. En conjunto, esto significa que el tamaño promedio para representar los mismos datos funcionales en el nuevo controlador es de 16 bytes en comparación con los 40 bytes anteriores, ¡una mejora bastante dramática!
Para plegar, veamos la cantidad de tablas y funclets únicos con el controlador antiguo y el nuevo:
Eh datos | Cuenta en __CxxFrameHandler3 | Cuenta en __CxxFrameHandler4 | % De reducción |
Entradas de datos | 12,322 | 9,855 | 20,0% |
Información de funciones | 6.386 | 2,747 | 57,0% |
Entradas de mapa IP2State | 6,363 | 2,148 | 66,2% |
Relájese entradas de mapa | 1,487 | 1,464 | 1,5% |
Capturar mapas de manejador | 2,603 | 601 | 76,9% |
Prueba mapas | 2,598 | 648 | 75,1% |
Dcle funclets | 2,301 | 1,527 | 33,6% |
Atrapar funclets | 2,603 | 84 | 96,8% |
Total | 36,663 | 19,074 | 48,0% |
El número de entradas únicas de datos EH disminuye en un 48% al crear oportunidades de plegado adicionales al eliminar los RVA y rediseñar los funclets de captura. Quiero mencionar específicamente el número de funclets de captura en cursiva en verde: se reduce de 2.603 a solo 84. Esto es una consecuencia de que C ++ / WinRT traduce HRESULT a excepciones de C ++ que genera muchos funclets de captura idénticos al código que ahora pueden ser doblado Ciertamente, una caída de esta magnitud está en el extremo superior de los resultados, pero sin embargo demuestra el potencial de ahorro de tamaño que se puede lograr cuando las estructuras de datos se diseñan teniendo esto en cuenta.
Rendimiento
Con el diseño que introdujo la compresión y la modificación de la ejecución en tiempo de ejecución, existía la preocupación de que se afectara el rendimiento de manejo de excepciones. Sin embargo, el impacto es positivo : el rendimiento de manejo de excepciones mejora con __CxxFrameHandler4 en comparación con __CxxFrameHandler3. Probé el rendimiento utilizando un programa de referencia que se desenrolla a través de 100 cuadros de pila cada uno con un try / catch y 3 objetos automáticos para destruir. Esto se ejecutó 50,000 veces para perfilar el tiempo de ejecución, lo que lleva a tiempos de ejecución generales de:
| __CxxFrameHandler3 | __CxxFrameHandler4 |
Tiempo de ejecución | 4.84s | 4.25s |
La creación de perfiles mostró que la descompresión introduce un tiempo de procesamiento adicional, pero su costo se ve superado por menos tiendas para el almacenamiento local de subprocesos en el nuevo diseño de tiempo de ejecución.
Planes futuros
Como se menciona en el título, FH4 actualmente solo está habilitado para binarios x64. Sin embargo, las técnicas descritas son extensibles a ARM32 / ARM64 y, en menor medida, x86. Actualmente estamos buscando buenos ejemplos (como Microsoft.UI.Xaml.dll) para motivar la extensión de esta tecnología a otras plataformas; si cree que tiene un buen caso de uso, ¡avísenos!
El proceso de integración de los cambios de tiempo de ejecución para las aplicaciones de la tienda para admitir FH4 está en marcha. Una vez hecho esto, el nuevo controlador se habilitará de manera predeterminada para que todos puedan obtener estos ahorros de tamaño binario sin esfuerzo adicional.
Palabras de clausura
Para cualquiera que piense que sus binarios x64 podrían reducirse un poco: ¡pruebe FH4 (a través de '/ d2FH4') hoy! Estamos entusiasmados de ver qué ahorros puede proporcionar esto ahora que esta función está disponible en la naturaleza. Por supuesto, si encuentra algún problema, infórmenos en los comentarios a continuación, por correo electrónico ( visualcpp@microsoft.com ) o a través de la Comunidad de desarrolladores . También nos puede encontrar en Twitter ( @VisualC ).
Gracias a Kenny Kerr por dirigirnos a Microsoft.UI.Xaml.dll, Ravi Pinjala por reunir los números en Office y Robert Roessler por probar esto en SQL.