前言:前文( 手把手写C++服务器(2):C/C++编译链接模型、函数重载隐患、头文件使用规范)研究了一些C++编译链接的基本原理,这篇文章继续探索优化方法,以及C++库的三种发布方式(动态库、静态库、源码库)。
目录
C++编译遇到的常见问题
(1)每个源文件独立编译,跨编译单元优化困难
(2)每个编译单元,都需要独立解析所有包含的头文件
(3)虚函数对编译带来的负担
如何减少代码编译时间?
(1)云编译、编译cache、并行编译等技术
(2)仅包含必要头文件,并尽量使用及提供前向声明版本的头文件
(3)gcc提供了不同等级的编译优化
(4)使用Pimpl,在公共接口里封装私有数据和方法
(5)C/C++ 跨编译单元的优化只能交给链接器
(6)优化不必要类之间的继承
C++库的三种发布方式:静态库、动态库和源码库
三种库的基本特性
debug build or release build?
参考
虽然C++运行效率很高,但是编译效率却让人大为苦恼。当项目代码一旦变多之后,编译带来的时间问题极大程度上影响了开发效率。如果不进行任何编译优化,十万余行的代码都能编译几个小时!C++编译常见的问题如下:
C/C++的编译系统和其他高级语言存在很大的差异,其他高级语言中,编译单元是整个Module,即Module下所有源码,会在同一个编译任务中执行。而在C/C++中,编译单元是以文件为单位。每个.c/.cc/.cxx/.cpp源文件是一个独立的编译单元,导致编译优化时只能基于本文件内容进行优化,很难跨编译单元提供代码优化。
如果N个源文件引用到了同一个头文件,则这个头文件需要解析N次。如果头文件中有模板(STL/Boost),则该模板在每个cpp文件中使用时都会做一次实例化,N个源文件中的std::vector会实例化N次。
编译器处理虚函数的方法是:给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了该类(包括继承自基类)的虚函数地址。如果派生类重写了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中。
调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。
使用虚函数后的变化:
虽然make、gcc等工具已经非常好用了,但是几乎每个大厂都有自己的编译工具,以我实习过的华为/百度为例,都是编译依赖全部上云,在云端进行cache、并行编译等优化,极大程度上提升了编译的速度。
头文件使用规范建议:
关于前向声明可参考:https://xduwq.blog.csdn.net/article/details/117291506
GCC提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对编译时间,目标文件长度,执行效率这个三维模型进行不同的取舍和平衡。优化的方法总体上将有以下几类:
如果全部了解这些编译选项,对代码针对性的优化还是一项复杂的工作,幸运的是GCC提供了从O0-O3以及Os这几种不同的优化级别供大家选择,在这些选项中,包含了大部分有效的编译优化选项,并且可以在这个基础上,对某些选项进行屏蔽或添加,从而大大降低了使用的难度。
Pimpl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法。这个技巧可以避免在头文件中暴露私有细节,因此是促进API接口与实现保持完全分离的重要机制。但是Pimpl并不是严格意义上的设计模式(它是受制于C++特定限制的变通方案),这种惯用法可以看作桥接设计模式的一种特例。
/* PublicClass.h */
#include
class PublicClass {
public:
PublicClass(); // Constructor
PublicClass(const PublicClass&); // Copy constructor
PublicClass(PublicClass&&); // Move constructor
PublicClass& operator=(const PublicClass&); // Copy assignment operator
PublicClass& operator=(PublicClass&&); // Move assignment operator
~PublicClass(); // Destructor
// Other operations...
private:
struct CheshireCat; // Not defined here
std::unique_ptr d_ptr_; // Opaque pointer
};
/* PublicClass.cpp */
#include "PublicClass.h"
struct PublicClass::CheshireCat {
int a;
int b;
};
PublicClass::PublicClass()
: d_ptr_(std::make_unique()) {
// Do nothing.
}
PublicClass::PublicClass(const PublicClass& other)
: d_ptr_(std::make_unique(*other.d_ptr_)) {
// Do nothing.
}
PublicClass::PublicClass(PublicClass&& other) = default;
PublicClass& PublicClass::operator=(const PublicClass &other) {
*d_ptr_ = *other.d_ptr_;
return *this;
}
PublicClass& PublicClass::operator=(PublicClass&&) = default;
PublicClass::~PublicClass() = default;
https://en.wikipedia.org/wiki/Opaque_pointer
|
使用impl 实现类 |
不使用impl实现类 |
优点 |
类型定义与客户端隔离, 减少#include 的次数,提高编译速度,库端的类随意修改,客户端不需要重新编译 |
直接,简单明了,不需要考虑堆分配,释放,内存泄漏问题 |
缺点 |
对于impl的指针必须使用堆分配,堆释放,时间长了会产生内存碎片,最终影响程序运行速度, 每次调用一个成员函数都要经过impl->xxx()的一次转发 |
库端任意头文件发生变化,客户端都必须重新编译 |
第一点说到C++的基本编译单元是单独的文件,编译优化只能针对单独的文件进行。因此跨单元的优化只能交给链接器解决。
链接器的工作流程:首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址,最后把所有的目标文件的内容写在各自的位置上,就生成一个可执行文件。链接的细节比较复杂,链接阶段是单进程,无法并行加速,导致大项目链接极慢。
通过final关键字禁止继承,因为每一次继承,编译器都会产生额外的开销!
动态库 | 静态库 | 源码库 | |
发布方式 | 头文件和.so文件 | 头文件和.a文件 | 头文件和.cc文件 |
编译时间 | 短 | 短 | 长 |
查询依赖 | ldd查询 | 编译期信息 | 编译期信息 |
部署方式 | 可执行文件+动态库 | 单一可执行文件 | 单一可执行文件 |
主要时间差 | 编译期——运行期 | 编译库——编译应用程序 | 无 |
Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息。 现代的编译器非常先进,在编译的过程会对编译进行非常多的优化,但是debug模式中将不会保存任何优化,为开发人员提供强大的应用程序调试能力。而Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利。
debug跟release在初始化变量时所做的操作是不同的,debug是将每个字节位都赋成0xcc, 而release的赋值近似于随机。如果你的程序中的某个变量没被初始化就被引用,就很有可能出现异常:用作控制变量将导致流程导向不一致;用作数组下标将会使程序崩溃;更加可能是造成其他变量的不准确而引起其他的错误。所以在声明变量后马上对其初始化一个默认的值是最简单有效的办法,否则项目大了你找都没地方找。代码存在错误在debug方式下可能会忽略而不被察觉到。debug方式下数组越界也大多不会出错,在release中就暴露出来了,这个找起来就比较难了。
只有debug版的程序才能设置断点、单步执行、使用 TRACE/ASSERT等调试输出语句。release不包含任何调试信息,所以体积小、运行速度快。
参考
- https://www.cnblogs.com/misserwell/p/4343927.html
- https://tech.meituan.com/2020/12/10/apache-kylin-practice-in-meituan.html
- https://xduwq.blog.csdn.net/article/details/117291506
- https://blog.csdn.net/zhangxiao93/article/details/74518204
- https://www.cnblogs.com/misserwell/p/4343927.html