VisualStudio 2019预览3引入了一个新特性,以减少X64上的C++异常处理(TIG/catch和自动析构函数)的二进制大小。被称为FH4(对于*CXFrAMPANDROLR4,见下文),我开发了用于C++异常处理的数据的新格式和处理,即 60%% 比现有的实现更小,导致整体二进制减少多达 20%% 对于C++异常处理的大量使用程序。
* 更新日期:5/25/2019 *
由于后勤问题,我们无法在更新1中启用FH4默认值。在更新2的早期预览中,一切看起来都就绪,可以启用FH4默认值*。
此外,正如Paul在visualstudio2019rtm发行版下面的评论中所报告的那样,新的运行时没有正确地安装到system32中,只安装了visualstudio。这是我们在更新1中修复的RTM中的一个常规错误,对于RTM,请运行“C:Program Files(x86)Microsoft Visual Studio2019EnterpriseVcredMSVC14.20.27508vcu redist.x64.exe”在计划使用FH4运行二进制文件的任何计算机上安装运行时。
* 更新日期:7/25/2019 *
我们发现FH4的运行时中缺少用于调试的钩子。这只会导致调试器出现这样的问题:来自抛出的“单步执行”不再进入相应的catch,而来自catch的“单步执行”不再进入正常执行路径中的下一行(相反,它们都进入下一个断点/程序结尾)。这在16.2中已经被修复,但是由于UWP库的掉头不能及时用于16.2中的UWP运行时。考虑到这一点,决定在16.2版中不推FH4违约,而是等待16.3版,届时一切将齐头并进。昨天发布的16.3版本的预览版1中,FH4默认开启,计划继续前进。
怎么打开这个?
FH4目前 默认情况下关闭 因为存储应用程序所需的运行时更改无法进入当前版本。要为非存储应用程序打开FH4,请将未记录的标志“/d2FH4”传递给VisualStudio2019 Preview 3及更高版本中的MSVC编译器。
我们计划在商店运行时更新后默认启用FH4。我们希望在VisualStudio2019更新1中实现这一点,并在了解更多信息后更新此文章。
工具更改
VisualStudio 2019预览3和更远的任何安装都会有编译器和C++运行时的改变来支持FH4。编译器更改存在于上述“/d2FH4”标志下的内部。C++运行时运动一个新的DLL,称为VCRUNTEM140Y1.DLL,它由VCCRIST自动安装。这是公开新的异常处理程序CxxFrameHandler4所必需的,它将替换旧的CxxFrameHandler3例程。同时支持新的C++运行时的静态链接和App本地部署。
现在开始有趣的事情!本文的其余部分将介绍在Windows、Office和SQL上试用FH4的内部结果,然后介绍这项新技术背后更深入的技术细节。
动机和结果
大约一年前,我们的合伙人 C++/WIR T 项目来到微软C++团队面临一个挑战:我们能减少C++异常处理对于使用它的程序的二进制大小有多大?
在程序上下文中使用 C++/WRET 他们指出了一个Windows组件MyFas.U.xAML.DLL,这是众所周知的,由于C++异常处理,有很大的二进制占用空间。我确认确实如此,并使用现有的uucxframehandler3生成了二进制大小的细分,如下所示。图表右侧的百分比是 占总二进制大小的百分比 被特定的元数据表和大纲代码占用。
在这篇文章中,我将不讨论图表右侧的具体结构是做什么的(参见James McNellis关于如何做的演讲) 堆栈展开在Windows上工作 更多细节)。然而,纵观整个元数据和代码,一个惊人的 26.4%% 二进制大小是C++异常处理所使用的。这是一个巨大的空间,阻碍了C++的使用。
我们已经做了一些改变,以减少编译器中C++异常处理的大小,而不改变运行时。这包括删除不能抛出和折叠逻辑上相同状态的代码区域的元数据。然而,我们仅仅在编译器中就已经完成了我们所能做的事情,而不能在这么大的东西上做出重大的改变。分析表明,有重大的胜利,但需要在数据,代码和运行时的根本变化。所以我们继续做。
使用新的uu cxframehandler4及其附带的元数据,Microsoft.UI.XAML.dll的大小细分如下:
C++异常处理所使用的二进制大小由 64%% 导致总体二进制大小减少 18.6%% 在这个二进制文件上。每种类型的结构都以惊人的程度缩小:
EH数据 | __CxxFrameHandler3大小(字节) | __CxxFrameHandler4大小(字节) | %%尺寸减小 |
Pdata条目 | 147,864 | 118,260 | 20.0%% |
展开代码 | 224,284 | 92,810 | 58.6%% |
功能信息 | 255,440 | 27,755 | 89.1%% |
IP2状态映射 | 186,944 | 45,098 | 75.9%% |
展开贴图 | 80,952 | 69,757 | 13.8%% |
捕获处理程序映射 | 52,060 | 6,147 | 88.2%% |
试试地图 | 51,960 | 5,196 | 90.0%% |
Dtor functlets函数 | 54,570 | 45,739 | 16.2%% |
捕捉functlet | 102,400 | 4,301 | 95.8%% |
总计 | 1,156,474 | 415,063 | 64.1%% |
总之,切换到uuxframehandler4将Microsoft.UI.Xaml.dll的总大小从4.4 MB降到了3.6 MB。
在一组具有代表性的Office二进制文件上试用FH4表明,大量使用异常的dll的大小减少了约10%。即使在Word和Excel中(它们的设计目的是最大限度地减少异常使用),二进制文件的大小仍然有很大的减少。
二元的 | 旧大小(MB) | 新大小(MB) | %%尺寸减小 | 说明 |
图表.dll | 17.27 | 15.10 | 12.6%% | 支持与图表和图形交互 |
Csi.dll | 9.78 | 8.66 | 11.4%% | 支持使用 存储在云中的文件 |
Mso20Win32Client.dll | 6.07 | 5.41 | 11.0%% | 在所有Office应用程序之间共享的通用代码 |
Mso30Win32Client.dll | 8.11 | 7.30 | 9.9%% | 在所有Office应用程序之间共享的通用代码 |
oart.dll | 18.21 | 16.20 | 11.0%% | Office应用程序之间共享的图形功能 |
wwlib.dll | 42.15 | 41.12 | 2.5%% | Microsoft Word的主二进制文件 |
excel.exe | 52.86 | 50.29 | 4.9%% | Microsoft Excel的主二进制文件 |
在核心SQL二进制文件上试用FH4可以减少4-21%%的大小,主要是由于下一节所述的元数据压缩:
二元的 | 旧大小(MB) | 新大小(MB) | %%尺寸减小 | 说明 |
sqllang.dll | 47.12 | 44.33 | 5.9%% | 顶级服务:语言解析器、绑定器、优化器和执行引擎 |
sqlmin.dll | 48.17 | 45.83 | 4.8%% | 底层服务:事务和存储引擎 |
qds.dll | 1.42 | 1.33 | 6.3%% | 查询存储功能 |
SqlDK.dll文件 | 3.19 | 3.05 | 4.4%% | sqlos抽象:内存、线程、调度等。 |
自动管理.dll | 1.77 | 1.64 | 7.3%% | 数据库优化顾问逻辑 |
xedetours.dll | 0.45 | 0.36 | 21.6%% | 用于查询的飞行数据记录器 |
技术
当分析什么导致C++异常处理数据在微软中如此庞大时,我发现了两个主要的罪魁祸首:
- 数据结构本身很大:元数据表的大小是固定的,每个表有四个字节长的图像相对偏移量和整数字段。一个带有一个try/catch和一个或两个自动析构函数的函数有超过100字节的元数据。
- 生成的数据结构和代码不适合合并。元数据表包含图像相对偏移量,这些偏移量阻止了COMDAT折叠(链接器可以将相同的数据块折叠在一起以节省空间的过程),除非它们所表示的功能相同。此外,catch functlet(来自程序catch块的概述代码)不能折叠,即使它们的代码相同,因为它们的元数据包含在其父级中。
为了解决这些问题,FH4对元数据和代码进行了重组,以便:
- 以前的固定大小值已使用可变长度整数编码进行压缩,该编码将>90%的元数据字段从四个字节减少到一个字节。元数据表现在也是可变长度的,带有一个标头,用于指示是否存在某些字段以节省发出空字段时的空间。
- 所有可以是函数相对的图像相对偏移都是函数相对的。这允许COMDAT在具有相似特征的不同函数的元数据之间进行折叠(想想模板实例化),并允许压缩这些值。Catch functlet已经过重新设计,不再将其元数据存储在其父函数中,因此任何代码相同的Catch functlet现在都可以折叠为二进制文件中的单个副本。
为了说明这一点,让我们看一下用于uucxframehandler3的Function Info元数据表的原始定义。这是处理EH时运行时的起始表,并指向其他元数据表。此代码在任何VS安装中都是公开的,请查找
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;
此结构是固定大小的,包含10个字段,每个字段的长度为4字节。这意味着默认情况下需要C++异常处理的每个函数都会产生40字节的元数据。
现在进入新的数据结构(
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 };
请注意:
- 魔法数字已经被删除,当一个程序有数千个这样的条目时,每次都会发出0x19930522。
- 当C++中的动态异常规范被放弃支持时,EHFLAGS已经被移动到报头中,如果使用动态异常规范,编译器将默认为旧的*CXXFrAMFANDECHANLR3。
- 其他表的长度不再存储在“函数信息4”中。这使得COMDAT folding可以折叠更多的指向表,即使“Function Info 4”表本身无法折叠。
- (未明确显示)dispFrame和bbtFlags字段现在是可变长度整数。高级表示将其保留为uint32 以便于处理。
- 根据标头中设置的字段,可以省略bbtFlags、dispUnwindMap、dispTryBlockMap和dispFrame。
考虑到所有这些因素,新的“Function Info 4”结构的平均大小现在是13字节(1字节头+3个4字节图像相对于其他表的偏移量),如果不需要某些表,则可以进一步缩小。表的长度被移出,但是这些值现在被压缩了,并且在Microsoft.UI.Xaml.dll中发现90%的表可以容纳在一个字节内。综上所述,这意味着在新处理程序中表示相同函数数据的平均大小是16个字节,而之前的40个字节有了很大的改进!
对于折叠,让我们看看使用新旧处理程序的唯一表和functlet的数量:
EH数据 | 在CxxFrameHandler3中计数 | 在CxxFrameHandler4中计数 | %%还原 |
Pdata条目 | 12,322 | 9,855 | 20.0%% |
功能信息 | 6,386 | 2,747 | 57.0%% |
IP2State映射条目 | 6,363 | 2,148 | 66.2%% |
展开地图条目 | 1,487 | 1,464 | 1.5%% |
捕获处理程序映射 | 2,603 | 601 | 76.9%% |
试试地图 | 2,598 | 648 | 75.1%% |
Dtor functlets函数 | 2,301 | 1,527 | 33.6%% |
捕捉functlet | 2,603 | 84 | 96.8%% |
总计 | 36,663 | 19,074 | 48.0%% |
唯一EH数据项的数量减少 48%% 通过移除rva和重新设计catch functlet来创造额外的折叠机会。我特别想用绿色的方式来描述捕获函数的数量:从2603下降到只有84。这是C++ +WiRT将结果转化为C++异常的结果,它产生了许多代码,可以折叠。当然,这种幅度的下降是在结果的高端,但尽管如此,它还是证明了在设计数据结构时考虑到折叠可以实现的潜在规模节约。
性能
随着设计引入压缩和修改运行时执行,异常处理性能会受到影响。然而,其影响是一个巨大的问题 积极的 一:异常处理性能 提高 使用uuuxframehandler4而不是uuxframehandler3。我用一个 基准 程序 它通过100个堆栈帧展开,每个帧有一个try/catch和3个要销毁的自动对象。运行50000次以分析执行时间,从而得出以下总体执行时间:
__CXX框架句柄3 | __CXX框架处理程序4 | |
执行时间 | 4.84秒 | 4.25秒 |
分析表明,解压确实会带来额外的处理时间,但在新的运行时设计中,线程本地存储的存储较少,这会抵消解压的成本。
未来计划
如标题中所述,FH4目前仅对x64二进制文件启用。然而,所描述的技术可扩展到ARM32/ARM64,在较小程度上可扩展到x86。我们目前正在寻找一些好的例子(比如Microsoft.UI.Xaml.dll),如果您认为自己有一个好的用例,请告诉我们!
集成存储应用程序的运行时更改以支持FH4的过程正在进行中。完成后,新的处理程序将在默认情况下启用,这样每个人都可以无需额外的努力就可以获得这些二进制大小的节省。
闭幕词
对于那些认为自己的x64二进制文件可以进行一些精简的人:今天就试用FH4(通过’/d2FH4’)!我们很高兴地看到,现在这个功能已经面世,可以节省多少开支。当然,如果您遇到任何问题,请在下面的评论中通过电子邮件告知我们( visualcpp@microsoft.com ),或通过 开发者社区 . 你也可以在Twitter上找到我们( @视觉 ).
感谢Kenny Kerr指导我们访问Microsoft.UI.Xaml.dll,感谢Ravi Pinjala在Office上收集数据,感谢Robert Roessler在SQL上进行试验。