使C ++异常处理在x64上更小

Visual Studio 2019预览版3引入了一项新功能,以减少x64上C ++异常处理(try / catch和自动析构函数)的二进制大小。 我将其称为FH4(针对__CxxFrameHandler4,请参见下文),为用于C ++异常处理的数据开发了新的格式和处理,该格式和处理比现有实现小约60%,从而导致大量使用C ++的程序的总体二进制压缩减少了20%异常处理。


这篇文章在博客中

我该如何打开?


FH4当前默认情况下处于关闭状态,因为Store应用程序所需的运行时更改无法使其进入当前版本。 若要为非商店应用程序打开FH4,请将未记录的标志“ / d2FH4”传递给Visual Studio 2019 Preview 3及更高版本中的MSVC编译器。


我们计划在Store运行时更新后默认情况下启用FH4。 我们希望在Visual Studio 2019 Update 1中做到这一点,并将更新我们知道更多的这篇文章。


工具变更


Visual Studio 2019 Preview 3及更高版本的任何安装都将在编译器和C ++运行时中进行更改以支持FH4。 编译器更改在内部存在于上述“ / d2FH4”标志下。 C ++运行时运行了一个名为vcruntime140_1.dll的新DLL,该DLL由VCRedist自动安装。 公开新的异常处理程序__CxxFrameHandler4取代旧的__CxxFrameHandler3例程是必需的。 同时还支持新C ++运行时的静态链接和应用程序本地部署。


现在到有趣的东西! 本文的其余部分将涵盖在Windows,Office和SQL上试用FH4的内部结果,以及此新技术背后的更深入的技术细节。


动机和结果


大约一年前,我们在C ++ / WinR T项目上的合作伙伴向Microsoft C ++团队提出了一个挑战:对于大量使用C ++异常处理的程序,我们可以减少多少?


在使用C ++ / WinRT的程序的上下文中,他们指出了Windows组件Microsoft.UI.Xaml.dll,该组件由于C ++异常处理而具有很大的二进制足迹。 我确认确实如此,并使用现有的__CxxFrameHandler3生成了二进制大小的细分,如下所示。 图表右侧的百分比是特定元数据表和概述的代码所占二进制总大小的百分比


使用__CxxFrameHandler3的Microsoft.UI.Xaml.dll的大小细分


我不会在这篇文章中讨论图表右侧的具体结构(有关更多详细信息,请参阅James McNellis 关于Windows上的堆栈展开如何工作的演讲)。 然而,从总的元数据和代码来看,C ++异常处理使用了高达26.4%的二进制大小。 这是一个巨大的空间,并且阻碍了C ++ / WinRT的采用。


过去我们进行了更改,以减少编译器中C ++异常处理的大小,而不更改运行时。 这包括删除无法抛出并折叠逻辑上相同状态的代码区域的元数据。 但是,我们已经达到了只能在编译器中完成的工作的尽头,并且无法在如此大的功能上大打折扣。 分析表明,虽然可以取得重大胜利,但需要对数据,代码和运行时进行根本性的更改。 所以我们继续进行。


使用新的__CxxFrameHandler4及其随附的元数据,Microsoft.UI.XAML.dll的大小细分现在如下:


使用__CxxFrameHandler4的Microsoft.UI.Xaml.dll的大小细分


C ++异常处理使用的二进制大小减少了64%,导致此二进制文件的总体二进制大小减少了18.6% 。 每种结构的大小都会以惊人的程度缩小:

h数据__CxxFrameHandler3大小(字节)__CxxFrameHandler4大小(字节)缩小尺寸
Pdata条目147,864118,26020.0%
展开代码224,28492,81058.6%
功能信息255,44027,75589.1%
IP2状态图186,94445,09875.9%
展开地图80,95269,75713.8%
捕获处理程序映射52,0606,14788.2%
尝试地图51,9605,19690.0%
Dtor功能54,57045.73916.2%
赶上功能102,4004,30195.8%
合计1,156,474415,06364.1%

组合后,切换到__CxxFrameHandler4可以将Microsoft.UI.Xaml.dll的整体大小从4.4 MB减小到3.6 MB。


在一组具有代表性的Office二进制文件上试用FH4显示,在大量使用异常的DLL中,大小减小了约10%。 即使在旨在最大程度减少异常使用的Word和Excel中,二进制大小仍然有有意义的减小。

二元旧大小(MB)新大小(MB)缩小尺寸内容描述
chart.dll17.2715.1012.6%支持与图表交互
Csi文件9.788.6611.4%支持使用存储在云中的文件
Mso20Win32Client.dll6.075.4111.0%在所有Office应用之间共享的通用代码
Mso30Win32Client.dll8.117.309.9%在所有Office应用之间共享的通用代码
oart.dll18.2116.2011.0%Office应用程序之间共享的图形功能
wwlib.dll42.1541.122.5%Microsoft Word的主要二进制文件
excel.exe52.8650.294.9%Microsoft Excel的主要二进制文件

在核心SQL二进制文件上试用FH4可以将大小减小4-21%,这主要是由于下一节中介绍的元数据压缩所致:

二元旧大小(MB)新大小(MB)缩小尺寸内容描述
sqllang.dll47.1244.335.9%顶级服务:语言解析器,绑定程序,优化器和执行引擎
sqlmin.dll48.1745.834.8%低层服务:交易和存储引擎
qds.dll1.421.336.3%查询存储功能
SqlDK.dll3.193.054.4%SQL OS抽象:内存,线程,调度等。
autoadmin.dll1.771.647.3%数据库优化顾问逻辑
xedetours.dll0.450.3621.6%飞行数据记录器查询

科技


分析是什么原因导致Microsoft.UI.Xaml.dll中的C ++异常处理数据过大时,我发现了两个主要罪魁祸首:


  1. 数据结构本身很大:元数据表的大小是固定的,具有图像相对偏移量字段和整数,每四个字节长。 具有单个try / catch和一个或两个自动析构函数的函数具有超过100字节的元数据。
  2. 生成的数据结构和代码不适合合并。 元数据表包含相对于图像的偏移量,除非COMDAT表示的功能相同,否则它们会阻止COMDAT折叠(链接器可以将相同的数据折叠在一起以节省空间的过程)。 此外,即使捕获捕获函数(程序捕获块中的概述代码)与代码相同,也无法折叠,因为它们的元数据包含在其父级中。

为了解决这些问题,FH4重组了元数据和代码,使得:


  1. 先前的固定大小值已使用可变长度整数编码进行压缩,该可变长度整数编码将> 90%的元数据字段从四个字节减少到一个。 现在,元数据表的长度也是可变的,带有标题,以指示是否存在某些字段以节省发出空字段的空间。
  2. 所有可以是相对功能的图像相对偏移都已设为相对功能。 这允许COMDAT在具有相似特征的不同功能的元数据之间折叠(请考虑模板实例化),并允许压缩这些值。 捕获功能集已经过重新设计,不再将其元数据存储在其父代中,因此现在可以将任何代码相同的捕获功能集折叠成二进制文件中的单个副本。

为了说明这一点,让我们看一下用于__CxxFrameHandler3的Function Info元数据表的原始定义。 这是处理EH时运行时的起始表,并指向其他元数据表。 该代码在任何VS安装中都是公开可用的,请查找<VS安装路径> \ VC \ Tools \ MSVC \ <版本> \ 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; 

此结构的大小固定,包含10个字段,每个字段4个字节长。 这意味着默认情况下,每个需要C ++异常处理的函数都会产生40个字节的元数据。


现在到新的数据结构(<VS安装路径> \ VC \ Tools \ MSVC \ <版本> \ 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 }; 

注意:


  1. 当程序具有成千上万个此类条目时,幻数已被删除,每次发出0x19930522都会成为问题。
  2. 由于已放弃对C ++ 17中动态异常规范的支持,因此EHFlags已移至标头中,而dispESTypeList已被淘汰。 如果使用动态异常规范,则编译器将默认使用较旧的__CxxFrameHandler3。
  3. 其他表的长度不再存储在“功能信息4”中。 即使“ Function Info 4”表本身无法折叠,这也允许COMDAT折叠以折叠更多的指向表。
  4. (未明确显示)dispFrame和bbtFlags字段现在是可变长度的整数。 高级表示形式将其保留为uint32_t以便于处理。
  5. 根据标题中设置的字段,可以忽略bbtFlags,dispUnwindMap,dispTryBlockMap和dispFrame。

考虑到所有这些因素,新的“ Function Info 4”结构的平均大小现在为13个字节(1个字节的标题+三个相对于其他表的4个字节的图像相对偏移量),如果不需要某些表,则可以进一步缩小。 表的长度已移出,但现在这些值已压缩,并且Microsoft.UI.Xaml.dll中的90%被发现适合单个字节。 综上所述,这意味着在新处理程序中表示相同功能数据的平均大小为16个字节,而以前的40个字节是一个很大的改进!


对于折叠,让我们看一下新旧处理程序的唯一表和funclet的数量:

h数据计数__CxxFrameHandler3计数__CxxFrameHandler4减少百分比
Pdata条目12,3229,85520.0%
功能信息6,3862,74757.0%
IP2状态图条目6,3632,14866.2%
展开地图条目1,4871,4641.5%
捕获处理程序映射2,60360176.9%
尝试地图2,59864875.1%
Dtor功能2,3011,52733.6%
赶上功能2,6038496.8%
合计36,66319,07448.0%

通过删除RVA和重新设计捕获功能,创建更多折叠机会,唯一EH数据条目的数量下降了48% 。 我特别想指出用绿色斜体表示的catch函数的数量:它从2.603下降到只有84。这是C ++ / WinRT将HRESULT转换为C ++异常的结果,该异常生成了大量与代码相同的catch函数,现在可以折叠。 这种幅度的下降当然是在结果的高端,但是尽管如此,这表明在考虑到数据结构的设计时,可以实现潜在的节省空间的方法。


性能表现


随着设计引入压缩并修改运行时执行,人们担心异常处理性能会受到影响。 但是,其影响是积极的 :与__CxxFrameHandler3相比,使用__CxxFrameHandler4 可以提高异常处理性能。 我使用一个基准 程序测试了吞吐量,该程序可展开100个堆栈帧,每个堆栈都有一个try / catch和3个自动销毁的对象。 运行了50,000次以分析执行时间,从而导致总体执行时间为:

__CxxFrameHandler3__CxxFrameHandler4
执行时间4.84秒4.25秒

分析显示,解压缩确实会引入额外的处理时间,但是在新的运行时设计中,由于较少的存储到线程本地存储,其成本被抵消了。


未来计划


如标题中所述,FH4当前仅适用于x64二进制文件。 但是,所描述的技术可扩展到ARM32 / ARM64,在较小程度上可扩展到x86。 我们目前正在寻找良好的示例(例如Microsoft.UI.Xaml.dll),以鼓励将该技术扩展到其他平台-如果您认为自己有很好的用例,请告诉我们!


集成商店应用程序的运行时更改以支持FH4的过程正在进行中。 完成后,默认情况下将启用新的处理程序,以便每个人都可以省下这些二进制文件大小。


闭幕词


对于任何认为他们的x64二进制文件可以进行一些修整的人:今天尝试FH4(通过'/ d2FH4')! 我们很高兴地看到,既然此功能无处不在,它可以节省多少费用。 当然,如果您遇到任何问题,请通过电子邮件( visualcpp@microsoft.com )或通过Developer Community在以下评论中告知我们。 您也可以在Twitter( @VisualC )上找到我们。


感谢Kenny Kerr将我们定向到Microsoft.UI.Xaml.dll,感谢Ravi Pinjala收集了Office上的数字,并感谢Robert Roessler在SQL上进行了试用。

Source: https://habr.com/ru/post/zh-CN442996/


All Articles