关于c++:C服务编译耗时优化原理及实践

59次阅读

共计 10477 个字符,预计需要花费 27 分钟才能阅读完成。

一、背景

大型 C ++ 工程项目,都会面临编译耗时较长的问题。不论是开发调试迭代、准入测试,亦或是继续集成阶段,编译行为无处不在,升高编译工夫对进步研发效率来说具备十分重要意义。

美团搜寻与 NLP 部为公司提供根底的搜寻平台服务,出于性能的思考,底层的根底服务通过 C ++ 语言实现,其中咱们负责的深度查问了解服务(DeepQueryUnderstanding,下文简称 DQU)也面临着编译耗时较长这个问题,整个服务代码在优化前编译工夫须要二十分钟左右(32 核机器并行编译),曾经影响到了团队开发迭代的效率。基于这样的背景,咱们针对 DQU 服务的编译问题进行了专项优化。在这个过程中,咱们也积攒了一些优化的常识和教训,在这里分享给大家。

二、编译原理及剖析

2.1 编译原理介绍

为了更好地了解编译优化计划,在介绍优化计划之前,咱们先简略介绍一下编译原理,通常咱们在进行 C ++ 开发时,编译的过程次要蕴含上面四个步骤:

预处理器:宏定义替换,头文件开展,条件编译开展,删除正文。

  • gcc - E 选项能够失去预处理后的后果,扩大名为.i 或 .ii。
  • C/C++ 预处理不做任何语法查看,不仅是因为它不具备语法查看性能,也因为预处理命令不属于 C /C++ 语句(这也是定义宏时不要加分号的起因),语法查看是编译器要做的事件。
  • 预处理之后,失去的仅仅是真正的源代码。

编译器:生成汇编代码,失去汇编语言程序(把高级语言翻译为机器语言),该种语言程序中的每条语句都以一种规范的文本格式确切的形容了一条低级机器语言指令。

  • gcc - S 选项能够失去编译后的汇编代码文件,扩大名为.s。
  • 汇编语言为不同高级语言的不同编译器提供了通用的输入语言。

汇编器:生成指标文件。

  • gcc - c 选项能够失去汇编后的后果文件,扩大名为.o。
  • .o 文件,是依照的二进制编码方式生成的文件。

链接器:生成可执行文件或库文件。

  • 动态库:指编译链接时,把库文件的代码全副退出到可执行文件中,因而生成的文件比拟大,但在运行时也就不再须要库文件了,其后缀名个别为“.a”。
  • 动静库:在编译链接时并没有把库文件的代码退出到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可执行文件比拟小,动静库个别后缀名为“.so”。
  • 可执行文件:将所有的二进制文件链接起来交融成一个可执行程序,不论这些文件是指标二进制文件还是库二进制文件。

2.2 C++ 编译特点

(1)每个源文件独立编译

C/C++ 的编译系统和其余高级语言存在很大的差别,其余高级语言中,编译单元是整个 Module,即 Module 下所有源码,会在同一个编译工作中执行。而在 C /C++ 中,编译单元是以文件为单位。每个.c/.cc/.cxx/.cpp 源文件是一个独立的编译单元,导致编译优化时只能基于本文件内容进行优化,很难跨编译单元提供代码优化。

(2)每个编译单元,都须要独立解析所有蕴含的头文件

如果 N 个源文件援用到了同一个头文件,则这个头文件须要解析 N 次(对于 Thrift 文件或者 Boost 头文件这类动辄几千上万行的头文件来说,几乎就是“鬼故事”)。

如果头文件中有模板(STL/Boost),则该模板在每个 cpp 文件中应用时都会做一次实例化,N 个源文件中的 std::vector<int> 会实例化 N 次。

(3)模板函数实例化

在 C ++ 98 语言规范中,对于源代码中呈现的每一处模板实例化,编译器都须要去做实例化的工作;而在链接时,链接器还须要移除反复的实例化代码。显然编译器遇到一个模板定义时,每次都去进行反复的实例化工作,进行反复的编译工作。此时,如果可能让编译器防止此类反复的实例化工作,那么能够大大提高编译器的工作效率。在 C ++ 0x 规范中一个新的语言个性 — 内部模板的引入解决了这个问题。

在 C ++ 98 中,曾经有一个叫做显式实例化(Explicit Instantiation)的语言个性,它的目标是批示编译器立刻进行模板实例化操作(即强制实例化)。而内部模板语法就是在显式实例化指令的语法根底上进行批改失去的,通过在显式实例化指令前增加前缀 extern,从而失去内部模板的语法。

① 显式实例化语法:template class vector<MyClass>。
② 内部模板语法:extern template class vector<MyClass>。

一旦在一个编译单元中应用了内部模板申明,那么编译器在编译该编译单元时,会跳过与该内部模板申明匹配的模板实例化。

(4)虚函数

编译器解决虚函数的办法是:给每个对象增加一个指针,寄存了指向虚函数表的地址,虚函数表存储了该类(包含继承自基类)的虚函数地址。如果派生类重写了虚函数的新定义,该虚函数表将保留新函数的地址,如果派生类没有从新定义虚函数,该虚函数表将保留函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被增加到虚函数表中。

调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,应用类申明中定义的第几个虚函数,程序就应用数组的第几个函数地址,并执行该函数。

应用虚函数后的变动:

① 对象将减少一个存储地址的空间(32 位零碎为 4 字节,64 位为 8 字节)。
② 每个类编译器都创立一个虚函数地址表。
③ 对每个函数调用都须要减少在表中查找地址的操作。

(5)编译优化

GCC 提供了为了满足用户不同水平的的优化须要,提供了近百种优化选项,用来对编译工夫,指标文件长度,执行效率这个三维模型进行不同的取舍和均衡。优化的办法不一而足,总体上将有以下几类:

① 精简操作指令。
② 尽量满足 CPU 的流水操作。
③ 通过对程序行为地猜想,从新调整代码的执行程序。
④ 充沛应用寄存器。
⑤ 对简略的调用进行开展等等。

如果全副理解这些编译选项,对代码针对性的优化还是一项简单的工作,侥幸的是 GCC 提供了从 O0-O3 以及 Os 这几种不同的优化级别供大家抉择,在这些选项中,蕴含了大部分无效的编译优化选项,并且能够在这个根底上,对某些选项进行屏蔽或增加,从而大大降低了应用的难度。

  • O0:不做任何优化,这是默认的编译选项。
  • O 和 O1:对程序做局部编译优化,编译器会尝试减小生成代码的尺寸,以及缩短执行工夫,但并不执行须要占用大量编译工夫的优化。
  • O2:是比 O1 更高级的选项,进行更多的优化。GCC 将执行简直所有的不蕴含工夫和空间折中的优化。当设置 O2 选项时,编译器并不进行循环展开以及函数内联优化。与 O1 比较而言,O2 优化减少了编译工夫的根底上,进步了生成代码的执行效率。
  • O3:在 O2 的根底上进行更多的优化,例如应用伪寄存器网络,一般函数的内联,以及针对循环的更多优化。
  • Os:次要是对代码大小的优化,通常各种优化都会打乱程序的构造,让调试工作变得无从着手。并且会打乱执行程序,依赖内存操作程序的程序须要做相干解决能力确保程序的正确性。

编译优化有可能带来的问题:

调试问题:正如下面所提到的,任何级别的优化都将带来代码构造的扭转。例如:对分支的合并和打消,对专用子表达式的打消,对循环内 load/store 操作的替换和更改等,都将会使指标代码的执行程序变得面目全非,导致调试信息严重不足。

内存操作程序扭转问题:在 O2 优化后,编译器会对影响内存操作的执行程序。例如:-fschedule-insns 容许数据处理时先实现其余的指令;-fforce-mem 有可能导致内存与寄存器之间的数据产生相似脏数据的不统一等。对于某些依赖内存操作程序而进行的逻辑,须要做严格的解决后能力进行优化。例如,采纳 Volatile 关键字限度变量的操作形式,或者利用 Barrier 迫使 CPU 严格依照指令序执行。

(6)C/C++ 跨编译单元的优化只能交给链接器

当链接器进行链接的时候,首先决定各个指标文件在最终可执行文件里的地位。而后拜访所有指标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。而后遍历所有指标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的地位上填写实现地址,最初把所有的指标文件的内容写在各自的地位上,就生成一个可执行文件。链接的细节比较复杂,链接阶段是单过程,无奈并行减速,导致大我的项目链接极慢。

三、服务问题剖析

DQU 是美团搜寻应用的查问了解平台,外部蕴含了大量的模型、词表、在代码构造上,蕴含 20 多个 Thrift 文件,应用大量 Boost 处理函数,同时引入了 SF 框架,公司第三方组件 SDK 以及分词三个 Submodule,各个模块采纳动静库编译加载的形式,模块之间通过音讯总线做数据的传输,音讯总线是一个大的 Event 类,这样这个类就蕴含了各个模块须要的数据类型的定义,所以各个模块都会引入 Event 头文件,不合理的依赖关系造成这个文件被改变,简直所有的模块都会从新编译。

每个服务所面临的编译问题都有各自的特点,然而遇到问题的实质起因是相似的,联合编译的过程和原理,咱们从预编译开展、头文件依赖以及编译过程耗时 3 个方面对 DQU 服务编译问题进行了剖析。

3.1 编译开展剖析

编译开展剖析就是通过 C ++ 的预编译阶段保留的.ii 文件,查看通过开展后的编译文件大小,具体能够通过在 cmake 中指定编译选型“-save-temps”保留编译两头文件。

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(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time")

编译耗时后果输入:

3.4 剖析工具建设

通过下面的工具剖析能拿到几个编译数据:

① 头文件依赖关系及个数。
② 预编译开展大小及内容。
③ 各个文件编译耗时。
④ 整体链接耗时。
⑤ 能够计算出编译并行度。

通过这几个数据的输出咱们思考能够做个自动化剖析工具,找出优化点以及界面化展现。基于这个目标,咱们建设了全流程自动化剖析工具,可能主动剖析耗时共性问题以及 TopN 耗时文件。剖析工具解决流程如下图所示:

(1) 整体统计分析成果

具体字段阐明:

① cost_time 编译耗时,单位是秒。
② file_compile_size,编译两头文件大小,单位是 M。
③ file_name,文件名称。
④ include_h_nums,引入头文件个数,单位是个。
⑤ top_h_files_info,引入最多的 TopN 头文件。

(2)Top10 编译耗时文件统计

用来展现统计编译耗时最久的 TopN 文件,N 能够自定义指定。

(3)Top10 编译两头文件大小统计

通过统计和展现编译文件大小,用来判断这块是否合乎预期,这个是和编译耗时一一对应的。

(4)Top10 引入最多头文件的头文件统计

(5)Top10 头文件反复次数统计

目前,这个工具反对一键化生成编译耗时剖析后果,其中几个小工具,比方依赖文件个数工具曾经集成到公司的上线集成测试流程中,通过自动化工具查看代码改变对编译耗时的影响,工具的建设还在一直迭代优化中,后续会集成到公司的 MCD 平台中,能够主动剖析来定位编译耗时长的问题,解决其它部门编译耗时问题。

四、优化计划与实际

通过使用上述相干工具,咱们可能发现 Top10 编译耗时文件的共性,比方都依赖音讯总线文件 platform_query_analysis_enent.h,这个文件又间接间接引入 2000 多个头文件,咱们重点优化了这类文件,通过工具的编译开展,找出了 Boost 应用、模板类开展、Thrift 头文件开展等共性问题,并针对这些问题做专门的优化。此外,咱们也应用了一些业内通用的编译优化计划,并获得了不错的成果。上面具体介绍咱们采纳的各种优化计划。

4.1 通用编译减速计划

业内有不少通用编译减速工具(计划),无需侵入代码就能进步编译速度,十分值得尝试。

(1)并行编译

在 Linux 平台上个别应用 GNU 的 Make 工具进行编译,在执行 make 命令时能够加上 -j 参数减少编译并行度,如 make -j 4 将开启 4 个工作。在实践中咱们并不将该参数写死,而是通过 $(nproc) 办法动静获取编译机的 CPU 核数作为编译并发度,从而最大限度利用多核的性能劣势。

(2)分布式编译

应用分布式编译技术,比方利用 Distcc 和 Dmucs 构建大规模、分布式 C ++ 编译环境,Linux 平台利用网络集群进行分布式编译,须要思考网络时延与网络稳定性。分布式编译适宜规模较大的我的项目,比方单机编译须要数小时甚至数天。DQU 服务从代码规模以及单机编译时长来说,临时还不须要应用分布式的形式来减速,具体细节能够参考 Distcc 官网文档阐明。

(3)预编译头文件

PCH(Precompiled Header),该办法事后将罕用头文件的编译后果保存起来,这样编译器在解决对应的头文件引入时能够间接应用事后编译好的后果,从而放慢整个编译流程。PCH 是业内非常罕用的减速编译的办法,且大家反馈成果十分不错。在咱们的我的项目中,因为波及到很多 Shared Library 的编译生成,而 Shared Library 相互之间无奈共享 PCH,因而没有获得料想成果。

(4)CCache

CCache(Compiler Cache 是一个编译缓存工具,其原理是将 cpp 的编译后果保留在文件缓存中,当前编译时若对应文件无变动可间接从缓存中获取编译后果。须要留神的是,Make 自身也有肯定缓存性能,当指标文件已编译(且依赖无变动)时,若源文件工夫戳无变动也不会再次编译;但 CCache 是按文件内容做的缓存,且同一机器的多个我的项目能够共享缓存,因而实用面更大。

(5)Module 编译

如果你的我的项目是用 C ++ 20 进行开发的,那么祝贺你,Module 编译也是一个优化编译速度的计划,C++20 之前的版本会把每一个 cpp 当做一个编译单元解决,会存在引入的头文件被屡次解析编译的问题。而 Module 的呈现就是解决这一问题,Module 不再须要头文件(只须要一个模块文件,不须要申明和实现两个文件),它会将你的(.ixx 或者 .cppm)模块实体间接编译,并主动生成一个二进制接口文件。import 和 include 预处理不同,编译好的模块下次 import 的时候不会反复编译,能够大幅度提高编译器的效率。

(6)主动依赖剖析

Google 也推出了开源的 Include-What-You-Use 工具(简称 IWYU),基于 Clang 的 C /C++ 工程冗余头文件查看工具。IWYU 依赖 Clang 编译套件,应用该工具能够扫描出文件依赖问题,同时该工具还提供脚本解决头文件依赖问题,咱们尝试搭建了这套剖析工具,这个工具也提供自动化头文件解决方案,然而因为咱们的代码依赖比较复杂,有动静库、动态库、子仓库等,这个工具提供的优化性能不能间接应用,其它团队如果代码构造比较简单的话,能够思考应用这个工具剖析优化,会生成如下后果文件,领导哪些头文件须要删除。

>>> Fixing #includes in '/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 <boost/serialization/version.hpp>
+#include <boost/serialization/version.hpp>  // for BOOST_CLASS_VERSION
+#include <string>                       // for string
+#include <vector>                       // for vector
+
+#include "qa/data/file_buffer.h"        // for REG_TEMPLATE_FILE_HANDLER
​

4.2 代码优化计划与实际

(1)前置类型申明

通过剖析头文件援用统计,咱们发现我的项目中被援用最多的是总线类型 Event,而该类型中又搁置了各种业务须要的成员,示例如下:

#include“a.h”#include "b.h"
class Event {
// 业务 A, B, C ...
  A1 a1;
  A2 a2;
     // ...
  B1 b1;
  B2 b2;
  // ...
};

这导致 Event 中蕴含了数量宏大的头文件,在头文件开展后,文件大小达到 15M;而各种业务都会须要应用 Event,天然会重大连累编译性能。

咱们通过前置类型申明来解决这个问题,即不引入对应类型的头文件,只做前置申明,在 Event 中只应用对应类型的指针,如下所示:

class A2;
// ...
class Event {
// 业务 A, B, C ...
  shared_ptr<A1> a1;
  shared_ptr<A2> a2;
  // ...
  shared_ptr<B1> b1;
  shared_ptr<B2> b2;
  // ...
};

只有在真正应用对应成员变量时,才须要引入对应头文件;这样真正做到了按需引入头文件。

(2)内部模板

因为模板被应用时才会实例化这一个性,雷同的实例能够呈现在多个文件对象中。编译器要对每一处模板进行实例化,链接器还要移除反复的实例化代码。当在宽泛应用模板的我的项目中,编译器会产生大量的冗余代码,这会极大地减少编译工夫和链接工夫。C++ 11 新规范中能够通过内部模板来防止。

// util.h
template <typename T> 
void max(T) {...}
// A.cpp
extern template void max<int>(int);
#include "util.h"
template void max<int>(int); // 显式地实例化 
void test1()
{max(1);
}


在编译 A.cpp 的时候,实例化出一个 max<int>(int)版本的函数。

// B.cpp
#include "util.h"
extern template void max<int>(int); // 内部模板的申明
void test2()
{max(2);
}

在编译 B.cpp 的时候,就不再生成 max<int>(int)实例化代码,这样就节俭了后面提到的实例化,编译以及链接的耗时了。

(3)多态替换模板应用

咱们的我的项目重度应用词典相干操作,如加载词典、解析词典、匹配词典(各种花式匹配),这些操作都是通过 Template 模板扩大反对各种不同类型的词典。据统计,词典的类型超过 150 个,这也造成模板开展的代码量收缩。

template <class R>
class Dict {
public:
  // 匹配 key 和 condition,赋值给 record
  bool match(const string &key, const string &condition, R &record);  // 对每种类型的 Record 都会开展一次
private:
  map<string, R> dict;
};


侥幸的是,咱们词典的绝大部分操作都能够形象出几类接口,因而能够只实现针对基类的操作:

class Record {  // 基类
public:
  virtual bool match(const string &condition);  // 派生类需实现
};
​
class Dict {
public:
  shared_ptr<Record> match(const string &key, const string &condition);  // 应用方传入派生类的指针即可
private:
  map<string, shared_ptr<Record>> dict;
};


通过继承和多态,咱们无效防止了大量的模板开展。须要留神的是,应用指针作为 Map 的 Value 会减少内存调配的压力,举荐应用 Tcmalloc 或 Jemalloc 替换默认的 Ptmalloc 优化内存调配。

(4)替换 Boost 库

Boost 是一个宽泛应用的根底库,涵盖了大量罕用函数,非常不便、好用,然而也存在一些不足之处。一个显著毛病是其实现采纳了 hpp 的模式,即申明和实现均放在头文件中,这会造成预编译开展后非常微小。

// 字符串操作是罕用性能,仅仅引入该头文件开展大小就超过 4M
#include <boost/algorithm/string.hpp>
// 与此绝对的,引入多个 STL 的头文件,开展后仅仅只有 1M
#include <vector>
#include <map>
// ...

在咱们我的项目中次要应用的 Boost 函数不超过二十个,局部能够在 STL 中找到代替,局部咱们手动做了实现,使得我的项目从重度依赖 Boost 转变成绝大部分达到 Boost-Free,大大降低了编译的累赘。

(5)预编译

代码中有一些平时改变比拟少,然而对编译耗时产生肯定的影响,比方 Thrift 生成的文件,模型库文件以及 Common 目录下的通用文件,咱们采取提起预编译成动静库,缩小后续文件的编译耗时,也解决了局部编译依赖。

(6)解决编译依赖,进步编译并行度

在咱们我的项目中有大量模块级别的动静库文件须要编译,cmake 文件指定的编译依赖关系在肯定水平上限度了编译并行度的执行。

比方上面这个场景,通过正当设置库文件依赖关系,能够进步编译并行度。

4.3 优化成果

咱们通过 32C、64G 内存机器做了编译耗时优化前后的成果比照,统计后果如下:

4.4 守住优化成绩

编译优化是一件“逆水行舟”的事件,开发人员总是偏向于一直减少新的性能、新的库乃至新的框架,而要删除旧代码、旧库、下线旧框架总是困难重重(置信一线开发人员肯定深有体会)。因而,如何守住之前获得的优化成绩也是至关重要的。咱们在实践中有以下几点领会:

  • 代码审核是艰难的(引起编译耗时减少的改变,往往无奈通过审核代码直观地发现)。
  • 工具、流程才值得依赖。
  • 关键在于管制增量。

咱们发现,cpp 文件的编译耗时,和其预编译开展文件(.ii)大小呈正相干(绝大部分状况下);对每一个上线版本,将其所有 cpp 文件的预编译开展大小记录下来,就造成了其编译指纹(CF,Compile Fingerprint)。通过比拟相邻两个版本的 CF,就能较精确的晓得新版带来的编译耗时次要由哪些改变引入,并能够进一步剖析耗时上涨是否正当,是否有优化空间。

咱们将该种形式制作成脚本工具并引入上线流程,从而可能很分明的理解每次代码发版带来的编译性能影响,并无效地帮忙咱们守住后期的优化成绩。

五、总结

DQU 我的项目是美团搜寻业务环节中重要的一环,该零碎须要对接 20+RPC、数十个模型、加载超过 300 个词典,应用内存数十 G,日均响应申请超过 20 亿的大型 C ++ 服务。在业务高速迭代的状况,简短的编译工夫为开发同学带来较大的困扰,肯定水平上制约了开发效率。最终咱们通过编译优化剖析工具建设,联合采纳了通用编译优化减速计划和代码层面的优化,将 DQU 的编译工夫缩短了 70%,并通过引 CCache 等伎俩,使得本地开发的编译,可能在 100s 内实现,给开发团队节俭了大量的工夫。

在获得阶段性成绩之后,咱们总结整个问题解决的过程,并积淀出一些分析方法、工具以及流程标准。这些工具在后续的开发迭代过程中,可能疾速无效地检测新的代码变更带来的编译工夫变动,并成为了咱们的上线流程查看中的一环检测规范。这一点与咱们以往一次性的或者针对性的编译优化,产生了很大的区别。毕竟代码的保护是一个长久的过程,系统化的解决这一问题,不只是须要无效的办法和便捷的工具,更须要一个标准化的,规范化的上线流程来放弃成绩。心愿本文对大家能有所帮忙。

参考文献

  • [1]《编译原理透视·图解编译原理》
  • [2] CCache
  • [3] 分布式编译
  • [4] 头文件预编译
  • [5] 头文件预编译
  • [6] C++ Templates
  • [7] Include-what-you-use

作者简介

本文作者周磊、识瀚、朱超、王鑫、刘亮、昌术、李超、云森、永超等,均来自美团 AI 平台搜寻与 NLP 部。

| 想浏览更多技术文章,请关注美团技术团队(meituantech)官网微信公众号。

| 在公众号菜单栏回复【2019 年货】、【2018 年货】、【2017 年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。

正文完
 0