c++ 服务,c++ 编译优化
如果头文件中有模板(STL/Boost),则每个cpp文件中使用该模板时都会实例化一次,而N个源文件中的std:vector会实例化N次。
3.实例化模板函数
C++98语言标准要求编译器对源代码中出现的每一个模板实例化都执行实例化工作,并且在链接时,链接器必须去除重复的实例化代码。显然,编译器每次遇到模板定义时都会重复执行实例化和编译工作。这时,如果编译器能够避免这种重复的实例化工作,那么编译器的工作效率就可以大大提高。通过引入外部模板(C++0x 标准中的一项新语言功能)解决了这个问题。
C++98 已经具有称为显式实例化的语言功能。其目的是指示编译器立即执行模板实例化操作(即强制实例化)。外部模板的语法是在显式实例化指令的语法基础上修改的,外部模板的语法是在显式实例化指令前添加前缀extern 得到的。
显式实例化语法:模板类向量。 外部模板语法:extern 模板类向量。
当编译单元中使用外部模板声明时,编译器在编译该编译单元时会跳过与外部模板声明匹配的模板的实例化。
4. 虚函数
编译器处理虚函数的方式是为每个对象添加一个指针,并存储一个指向虚函数表的地址。虚函数表存储类的虚函数地址(包括从基类继承的虚函数地址)。当派生类重写虚函数的新定义时,虚函数表存储新函数的地址。如果派生类没有重新定义虚函数,则虚函数表将保留原始版本的地址。功能。当派生类定义新的虚函数时,该函数的地址将添加到虚函数表中。
当调用虚函数时,程序会检查对象中存储的虚函数表的地址,引用相应的虚函数表,并使用类声明中定义的虚函数。该程序使用数组的函数地址。并运行这个函数。
使用虚函数后的变化:
该对象提供额外的空间来存储地址(32 位系统为4 个字节,64 位系统为8 个字节)。 每个类编译器都会创建一个虚函数地址表。 对于每个函数调用,都需要添加一个在表中查找地址的操作。
5. 编译优化
为了满足不同级别用户的优化需求,GCC 提供了大约100 个优化选项,以在编译时间、目标文件长度和执行效率的三维模型中平衡不同的权衡。优化方法有很多,一般分为以下几类:
精简操作说明。 确保满足CPU流水线操作。 估计程序的行为并重新调整代码执行顺序。 掌握登记册。 扩展简单调用等。
即使您了解所有这些编译选项,有针对性地优化代码仍然是一项复杂的任务。幸运的是,GCC提供了从O0到O3和Os的几种不同的优化级别供大家选择。这些选项中包括大多数有效的编译优化。选项是可用的,在此基础上可以屏蔽或添加一些选项,大大降低了使用难度。
O0:不进行优化。这是默认的编译选项。 O和O1:对程序进行部分编译优化。编译器尝试减少生成代码的大小和执行时间,但不会执行需要大量编译时间的优化。 O2:比O1 更高级的选项,具有更多优化。 GCC 执行几乎所有不涉及时间和空间权衡的优化。设置O2 选项时,编译器不执行循环展开和函数内联优化。相比O1,O2优化增加了编译时间,提高了生成代码的执行效率。 O3:在O2的基础上进一步优化,包括使用伪寄存器网络、内联正则函数、进一步优化循环。 os:主要是优化代码大小,但各种优化通常会破坏程序的结构,导致调试困难。依赖内存操作顺序的程序必须进行相关处理,以保证程序的正确性。编译优化可能引起的问题:
调试问题:如上所述,任何级别的优化都会导致代码结构的变化。例如,合并和删除分支、移除公共子表达式、替换和改变循环内的加载/存储操作等,都会将目标代码的执行顺序改变得面目全非,导致调试信息明显缺失。
内存操作重排序问题:O2优化后,编译器影响了内存操作的执行顺序。例如:-fschedule-insns允许在处理数据时其他指令先完成,但是-fforce-mem引入了内存和寄存器之间数据不匹配的可能性,比如脏数据就有。某些依赖于内存操作序列的逻辑在执行优化之前需要进行严格的处理。例如,使用Volatile 关键字来限制对变量的操作,或者使用Barrier 来强制CPU 严格遵循指令序列。
6. C/C++交叉编译单元的优化可以只留给链接器
当链接器执行链接时,它首先确定每个目标文件在最终可执行文件中的位置。然后,它访问每个目标文件的地址重定义表,并重定向其中记录的地址(以及偏移量,即可执行文件上编译单元的起始地址)。然后遍历所有目标文件的未解析符号表,在所有导出的符号表中搜索匹配的符号,在未解析符号表记录的位置中输入实现地址,最后将所有目标文件的内容写入各自的位置。将会生成一个可执行文件。链接的细节相对复杂,并且链接阶段是一个单一的过程,无法并行加速,使得大型项目的链接速度非常慢。
3. 服务问题分析DQU 是美团搜索使用的查询理解平台,包含海量模型、词汇、代码结构的20 多个Thrift 文件,使用大量Boost 处理函数,并使用SF 框架进行工作。公司的第三方组件SDK和三个分词子模块。每个模块都采用动态库编译和加载的方式。数据通过消息总线在模块之间发送。由于消息总线是一个大的Event类,这个类包含了各个模块的需求。通过定义数据类型,每个模块引入一个事件头文件。不合理的依赖关系会导致这个文件发生变化,几乎所有模块都需要重新编译。
虽然每个服务面临的编译问题都是独特的,但问题的根本原因是相似的,都是基于编译过程和原则的组合:预编译扩展、头文件依赖和编译处理时间。我们将处理这要从两个方面来说。分析了DQU服务编译问题。
3.1 编译部署分析
编译和部署分析是一个.ii文件,在C++的预编译阶段维护。解压后检查编译文件的大小。具体来说,您可以通过指定编译选项“-save-temps”来保留编译中间文件。 cmake。
set(CMAKE_CXX_FLAGS'-std=c++11${CMAKE_CXX_FLAGS}-ggdb-Og-fPIC-w-Wl,--export-dynamic-Wno-deprecated-fpermissive-save 编译时间长的最直接原因) -temps') 这意味着解压后编译出来的文件会比较大。分析编译解包后的文件大小和内容,预编译和预解包分析显示,解包后的文件超过40万行。你会注意到,Boost库引用和头文件引用很多,而且展开的文件比较大,所以编译时间会受到影响。可以看到,每个文件的编译时间都差不多,不过下图是编译扩容后的文件大小截图。
3.2 头文件依赖分析
头文件依赖分析是从引用头文件数量的角度来判断代码是否有效的分析方法,通过分析并输出文件依赖关系的引用数量来支持。判断头文件依赖是否有效。
1、头文件引用总数统计
使用该工具统计编译后的源文件直接和间接依赖的头文件总数,并根据引入的头文件数量分析问题。
2、单个头文件的依赖统计
使用工具分析头文件依赖关系,生成依赖拓扑图,直观地识别不合理的依赖关系。
该图包括参考层次结构和参考头文件的数量。
3.3 长编译结果的分段统计
分段编译时间统计是基于每个文件的编译时间和每个编译阶段的时间的结果。这是一个直观的结果。一般情况下,和文件扩展后的大小,以及头文件引用的次数有正相关关系,是的,cmake允许你指定环境变量来输出编译和链接每一步需要多长时间的信息,这个数据可用于直观地分析耗时情况。
set_property(GLOBALPROPERTYRULE_LAUNCH_COMPILE'${CMAKE_COMMAND}-Etime')set_property(GLOBALPROPERTYRULE_LAUNCH_LINK'${CMAKE_COMMAND}-Etime') 输出需要很长时间编译的结果:
3.4 构建分析工具
通过上面提到的工具分析,可以得到一些编译数据。
头文件依赖关系和数量。 编译后的扩展大小和内容。 编译每个文件都需要时间。 创建整个链接需要时间。 可以计算编译并行度。
考虑创建一个自动分析工具来输入这些数据、发现优化点并显示界面。为此,我们构建了全流程自动化分析工具,可以自动分析常见耗时问题和耗时TopN文件。分析工具的处理流程如下图所示。
1、总体统计分析效果
具体字段说明:
cost_time是编译时间(秒)。 file_compile_size 编译时中间文件的大小,单位为M。 file_name,文件名。 include_h_nums 为要安装的头文件数量,单位为1。 top_h_files_info,介绍最多TopN个头文件。
2. 编译时间较长的文件前10名统计
用于显示统计时间最长的TopN文件。 N 是可定制的。
3. Top 10 编译中间文件大小统计
用于通过统计并显示编译文件的大小(对应于编译时间)来确定这项工作是否符合预期。
4. 部署最多的前10 个头文件统计
5. Top 10 头文件重复次数统计
目前该工具支持一键生成分析结果,编译耗时较长。几个较小的工具,例如依赖文件计数工具,已集成到公司的在线集成测试流程中。通过自动工具检查代码更改对编译时间和消耗的影响。该工具构建仍在迭代优化中,未来将集成到公司的MCD平台中。您可以自动分析和识别编译时间长的问题并解决它们。其他部门的编辑时间。
4. 优化规划和实践使用上面的相关工具,您可以找到编译时间最长的前10 个文件的共同点。例如,它们都依赖于消息总线文件platform_query_analysis_enent.h。该文件直接和间接包含2000 多个头文件。我们专注于优化此类文件。通过编译和扩展该工具,发现了常见问题并进行了特殊优化,例如使用Boost、扩展模板类、扩展Thrift头文件等。解决这些问题。另外,我们还使用了一些业界通用的编译优化方案,并取得了不错的效果。我们采用的各种优化解决方案详述如下。
4.1 常见的编译加速方案
业界有很多流行的编译加速工具(解决方案),可以在不侵入代码的情况下提高编译速度,因此值得尝试。
1. 并行编译
在Linux平台上,通常使用GNU Make工具进行编译。运行make命令时,可以添加-j参数以提高编译并行度。例如,make -j 4 启动四个任务。在实践中,您可以通过$(nproc) 方法动态检索编译器CPU 核心数作为编译并发数,从而充分利用多核性能,而不是硬编码此参数。
2.分布式编译
使用分布式编译技术时要考虑网络延迟和网络稳定性,例如使用Distcc或Dmucs构建大型分布式C++编译环境,或者使用网络集群进行分布式编译的Linux平台都需要这么做。分布式编译适合大型项目,比如需要几个小时甚至几天的单机编译。从代码大小和单机编译时间的角度来看,在可预见的未来,DQU服务不需要以分布式方式加速。更多信息请参考Distcc官方文档。
3.预编译头文件
PCH(预编译头)是一种常用的方法,预先保存头文件的编译结果,以便编译器在处理相应头文件的引入时可以直接使用预编译结果,从而加快整个编译过程。 PCH是业界非常常用的加速编译的方法,大家的反馈都非常好。我们的项目涉及编译和生成大量的共享库,由于我们无法在共享库之间共享PCH,因此我们无法获得预期的结果。
4.C缓存
CCache(Compiler Cache)是一个编译缓存工具。它的原理是将cpp编译结果存储在文件缓存中。如果后续编译时不修改对应文件,则可以直接从缓存中获取编译结果。请注意,Make 本身也具有某些缓存功能。当目标文件编译时(没有依赖关系发生改变),如果源文件的时间戳没有改变,则不会再次编译,但CCache会根据以下信息对其进行缓存:缓存可以被一台机器上的多个项目共享,使其更加通用。
5. 编译模块
如果您的项目是使用C++20 开发的,那么恭喜您!编译模块也是优化编译速度的一种解决方案。在C++20 之前,每个cpp 都被视为一个编译单元并导入头文件。我重复了多次分析和编译。模块的引入就是为了解决这个问题。模块不需要头文件(它只需要一个模块文件,而不需要两个文件来声明和实现)。 Module 编译(.ixx 或.cppm)模块。直接获取实体并自动生成二进制接口文件。导入和包含预处理是不同的。已编译的模块下次导入时不再重新编译,大大提高了编译效率。
6. 自动依赖分析
Google 还推出了开源的Include-What-You-Use 工具(称为IWYU),这是一个基于Clang 的C/C++ 项目冗余头文件检查工具。 IWYU 依赖于Clang 编译套件。您可以使用此工具扫描文件是否存在依赖性问题。同时,该工具还提供了解决头文件依赖问题的脚本。我们着手构建这个分析工具集。该工具还提供了自动化头文件解决方案,但由于代码依赖关系比较复杂,包括动态库、静态库、子仓库等,因此无法直接使用该工具提供的优化功能。如果对方团队的代码结构比较简单,可以考虑使用这个工具来分析和优化,它会产生下面的结果文件以及要删除哪些头文件,告诉你需要做什么。
修复#includesin'/opt/meituan/zhoulei/query_analysis/src/common/qa/record/brand_record.h'@@-1,9+1,10@@#ifndef_MTINTENTION_DATA_BRAND_RECORD_H_#define_MTINTENTION_DATA_BRAND_RECORD_H_-#include'qa/data/record .h'-#include'qa/data/template_map.hpp'-#include'qa/data/template_vector.hpp'-#include+#include//forBOOST_CLASS_VERSION+#include//forstring+#include//forvector++#include'qa/data/file_buffer.h'//forREG_TEMPLATE_FILE_HANDLER4.2代码优化规划与实践
1.前言类型声明
通过分析头文件引用统计,我们发现项目中引用最多的是总线类型Event,并且该类型包含了各种业务需求所需的成员。
#include "a.h" #include 'b.h'classEvent{//业务A,B,C.A1a1;A2a2;//.B1b1;B2b2;//.};这样就会引发该事件它发生了。其中包含了大量的头文件,当头文件展开时,文件大小达到15M,并且需要使用事件来完成各种任务,这当然会显着降低编译性能。
前向类型声明解决了这个问题。即不引入对应类型的头文件,只创建前向声明,并且在事件中只使用对应类型的指针,如下所示。
只需要实际使用对应的成员变量时才需要安装对应的头文件,并且可以按需安装头文件。
2. 外部模板
模板在使用时实例化,因此同一实例可以出现在多个文件对象中。编译器实例化每个模板,链接器删除重复的实例化代码。当处理广泛使用模板的项目时,编译器可能会生成大量冗余代码,这会显着增加编译和链接时间。通过使用新C++11 标准中的外部模板可以避免这种情况。
编译A.cpp 时实例化该函数的max(int) 版本。
//B.cpp#include'util.h'externtemplatevoidmax(int);//外部模板声明voidtest2(){max(2);} 编译B.cpp时,将不再生成max(int)实例。它可以为您节省上述耗时的实例化、编译和链接。
3. 使用多态替换模板
我们的项目经常用到字典相关的操作,比如字典加载、字典解析、字典匹配(各种花式匹配)。通过模板扩展,各种类型的词典都支持这些操作。据统计,字典类型超过150种,这也增加了模板扩展的代码量。
templateclassDict{public://匹配key和condition并赋值给record boolmatch(conststringkey,conststringcondition,Rrecord);//每种类型的Record扩展一次private:mapdict;}; 幸运的是,大多数对字典的操作都是可以抽象出来的。既然是接口,就只能实现基类的操作。
classRecord{//基类public:virtualboolmatch(conststringcondition);//派生类必须实现}; classDict{public:shared_ptrmatch(conststringkey, conststringcondition);//用户可以向派生类传递指针private:mapdict;}; 通过继承和多态,有效避免大量模板扩展。请注意,使用指针作为映射值会增加内存分配成本。我们建议使用Tcmalloc 或Jemalloc 替换默认的Ptmalloc 以优化内存分配。
4.替换Boost库
Boost是一个广泛使用的基础库,涵盖了大量常用功能,非常有用且易于使用,但它确实有一些缺点。一个显着的缺点是它的实现采用hpp 的形式。这意味着声明和实现都放在头文件中,使得部署后预编译相当广泛。
//字符串操作是常用函数,但是仅仅引入这个头文件就会将大小扩大到4M以上#include //另一方面,即使引入多个STL头文件,也会将大小增加到1M#include#include//.我们的项目主要使用了不到20个Boost函数,其中一些可以用STL替换,还有一些我们手动实现。这使得原本严重依赖Boost 的项目现在大部分都无需使用Boost,显着降低了成本。创建时间也有负担。
5. 预编译
代码上有一些相对较小的改动,但确实对编译时间有一些影响,包括Thrift生成的文件、模型库文件以及Common目录下的公共文件。编译后续文件需要时间,并且还要解决一些编译依赖关系。
6.解决编译依赖,提高编译并行性。
在我们的项目中,有很多模块级的动态库文件需要编译,但是cmake文件中指定的编译依赖在一定程度上限制了编译的并行执行。
例如,在以下场景中,正确设置库文件依赖关系可以提高编译并行性:
4.3 优化效果
我们使用32C和64G内存的机器来比较编译时间较长的优化前后的效果。统计结果如下。
4.4 保留优化结果
编译优化是一个“逆流而行”的问题。开发者总是倾向于不断地添加新功能、新库、甚至新框架,而去除旧代码、旧库、下线旧框架总是很困难(一线开发必须有深厚的经验)。因此,如何保持之前取得的优化结果也很重要。我们实际上有以下经验:
代码审查很困难(增加编译时间的更改通常在代码审查中不可见)。工具和流程值得信赖。关键是控制增量。我们发现,cpp 文件的编译时间(在大多数情况下)与预编译扩展文件(.ii) 的大小正相关。对于每个在线版本,都会记录所有cpp文件的预编译扩展大小。创建指纹(CF,编译指纹)。通过比较CF的两个相邻版本,您可以更好地了解新版本导致编译时间变化的主要原因,以及时间增加是否合理或是否有优化的空间。您可以进一步了解分析一下有没有。
通过将该方法作为脚本工具并将其引入到在线流程中,我们现在可以清楚地了解每次代码发布对编译性能的影响,并有效保留以前的优化结果。
5.总结DQU项目是美团搜索业务的关键部分,系统连接20多个RPC、数十个模型、加载300多个词典、使用数十GB内存、需要响应20多个1亿个请求。每天大规模C++ 服务。在业务迭代较快的情况下,编译时间会较长,会给开发同学带来很大的不便,一定程度上限制了开发效率。最终,通过构建编译优化分析工具,结合通用编译优化加速方案和代码级优化,我们能够将DQU的编译时间减少70%。开发可以在100 秒之内完成,为您的开发团队节省大量时间。
在取得阶段性结果后,我们总结了整个解决问题的过程,并整理了几种分析方法、工具和流程规范。在后续的开发迭代中,这些工具已经能够快速有效地检测新代码变更引起的编译时变化,并成为在线流程检查中的检测标准。这与传统的一次性或有针对性的编译优化有很大不同。毕竟,代码维护是一个长期的过程,系统地解决这个问题不仅需要有效的方法和方便的工具,还需要一个标准化的在线流程来维护结果。我希望这篇文章对您有所帮助。
作者简介周雷、史涵、朱超、王欣、刘良、常树、李超、云森、超勇均来自美团人工智能平台搜索与自然语言处理部。 - - - - - 结尾-
--------- 招聘信息美团搜索与NLP部,长期招聘搜索、推荐、NLP算法工程师,坐标北京/上海。感兴趣的同学可投递简历至:tech@meituan.com(邮件主题请注明:搜索与NLP部)。 | 欢迎关注:美团技术团队微信公众号(meituantech),我们会定期推送技术干货、技术沙龙和技术团队招聘信息。| 在公众号菜单栏回复【2019年货】、【2018年货】、【2017年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。阴阳师4月22日更新内容:帝释天上线技能调整,红莲华冕活动来袭[多图],阴阳师4月22日更新的内容有哪些?版本更新
2024-04-06四川电视台经济频道如何培养孩子的学习习惯与方法直播在哪看?直播视频回放地址[多图],2021四川电视台经济频
2024-04-06湖北电视台生活频道如何培养孩子的学习兴趣直播回放在哪看?直播视频回放地址入口[多图],湖北电视台生活频道
2024-04-06