|
c++的性能优化问题,之前积累了很多碎片的东西,现在统一整理一下,做成一个系列文章,供自己和他人参考。
一.函数调用效率低下原因
由于以下原因,函数调用可能会减慢程序速度:
1.1 函数调用使微处理器跳转到不同的代码地址并返回。这最多可能需要 4 个时钟周期。在大多数情况下,微处理器能够将调用和返回操作与其他计算重叠以节省时间。
1.2 如果代码碎片化并散布在各处,则代码缓存的工作效率会降低.
1.3 函数参数以32 位模式存储在堆栈中。将参数存储在堆栈并再次读取它们需要额外的时间。延迟是显着的,如果参数是关键依赖链的一部分。
1.4 需要额外的时间来设置栈帧、保存和恢复寄存器,以及可能保存异常处理信息。
1.5 每个函数调用语句在分支目标缓冲区(BTB) 中占用一个空间。如果程序的关键部分,有很多调用和分支处理逻辑,则 BTB 中的竞争会导致分支预测错误,接着会重新再次执行分支预测,直到命中。无疑会减慢程序速度。
二.函数调用优化措施
以下方法可用于减少在函数调用上花费的时间:
2.1 避免使用不必要的函数
一些编程教科书建议每个超过几行的函数,应该分成多个功能。这条规则太武断,更多的从代码重构方面考虑,对于性能方面考虑不足。将函数拆分为多个较小的函数只会降低程序的效率。因为它很长,对它进行拆分成一个函数,并不能使程序更清晰,除非函数正在做多个逻辑上不同的任务。如果可能,在一个函数中,最好保留一个关键的最内层循环。
2.2 使用内联函数
内联函数会在调用处即行展开,省略了该函数调用栈开销。所以对于提高程序性能很有帮助。
2.2.1 显式内联
使用 inline 关键字(实际上仅仅是向编译器建议),编译器可能在某些情况下忽略你的内联请求。(内联导致代码膨胀等)。
2.2.2 隐式内联
函数很小或仅从程序中的一个地方调用。这类函数通常由编译器自动内联,如class中定义的function。
2.3 避免在最内层循环中调用嵌套函数
调用其他函数的函数称为帧函数,而不调用其他函数的函数称为叶函数。叶函数比帧函数更高效。如果程序的关键最内层循环包含对帧函数的调用,那么可以通过内联来改进代码中的帧函数或通过内联所有的它调用的帧函数将帧函数变成叶函数。
2.4 使用宏而不是函数
用#define 声明的宏肯定是内联的。但要注意宏参数每次使用时都会对其进行评估。例子:
// 示例 7.34a。使用宏作为内联函数
#define MAX(a,b) (a > b ? a : b)
y = MAX(f(x), g(x));在这个例子中,f(x) 或 g(x) 被计算了两次,因为宏引用了它两次。您可以通过使用内联函数而不是宏来避免这种情况。如果你想让这个功能使用任何类型的参数,可以将其设为模板:
// 示例 7.34b。用模板替换宏
template<typename T>
static inline T max(T const & a, T const & b) {
return a > b ? a : b; }宏的另一个问题是名称不能重载或限制范围。 宏将干扰任何具有相同名称的函数或变量,无论作用域如何或命名空间。因此,特别是在头文件中为宏使用长且唯一的名称很重要。
2.5 使用 fastcall 和 vectorcall 函数
关键字 __fastcall 改变了 32 位模式下的函数调用方式(在 32位模式下函数参数默认在堆栈中传输),使得前两个整数参数在寄存器中传输,而不是在堆栈中传输。这个可以提高具有整数参数的函数的速度。浮点参数不受 __fastcall 的影响。成员函数中的隐式“this”指针也被视为参数,因此可能只有一个空闲寄存器(另外一个this占据了)留下来传输额外的参数。因此,当您使用 __fastcall 时,确保最关键的整数参数排在第一位。
在 64 位模式下函数参数默认在寄存器中传输。因此,__fastcall 关键字是在 64 位模式下无法识别。关键字__vectorcall改变了Windows系统中的函数调用方式浮点和向量操作数,以便在向量中传递和返回参数寄存器。这在 64 位 Windows 中特别有利。 __vectorcall 方法目前由 Microsoft、Intel 和 Clang 编译器支持。
2.6 将函数设为本地
仅在同一模块(即当前 .cpp 文件)中使用的函数应该是本地化。这使得编译器更容易内联函数和优化跨函数调用。可以通过三种方式使函数成为本地函数:
1.在函数声明中加入关键字static。这是最简单的方法,但它不适用于类成员函数,其中 static 具有不同的
意义。
2. 将函数或类放入匿名命名空间。
3. Gnu 编译器允许“__attribute__((visibility(&#34;hidden&#34;)))”。
2.7 使用全程序优化
一些编译器有一个选项可以优化整个程序或组合多个.cpp 文件到单个目标文件中。这使编译器能够优化寄存器分配以及跨越构成程序的所有 .cpp 模块的参数传输。整个程序优化不能用于作为对象或库文件分发的函数库。
2.8 使用 64 位模式
参数传递在 64 位模式下比在 32 位模式下更高效,在64 位 Linux 优于 64 位 Windows。在 64 位 Linux 中,前六个整数参数和前八个浮点参数在寄存器中传输,总共有十四个寄存器参数。在 64 位 Windows 中,前四个参数在寄存器中传输,不管它们是整数还是浮点数。因此,64 位 Linux 是如果函数有四个以上的参数,则比 64 位 Windows 更有效。32 位 Linux 和 32 位 Windows 在这方面没有区别。大量的寄存器提高了性能,因为编译器可以将变量存储在寄存器中而不是内存中。
2.9 递归函数采用尾递归
尾递归的效率比普通递归函数高,此外普通的递归函数存在爆栈溢出的风险,这时尾递归是有效的解决的办法。
三.函数参数优化措施
函数参数传递一般来说,分为三种。by value ,by pointer, by reference.下面依次介绍。
在大多数情况下,函数参数是按值传递的。这意味着参数被复制到局部变量。
3.1 参数是简单类型
这对于简单类型(如 int、
float、double、bool、enum 以及指针和引用来说,按值传递很有效率。
数组参数总是作为(退化)指针传输,除非它们被包装到类或结构中。
3.2 参数是复合类型
如果参数具有复合类型(例如结构体或类。如果满足以下所有条件,复合类型参数按值传递的传输效率最高
满足条件:
• 对象很小,可以放在一个寄存器中
• 对象没有复制构造函数也没有析构函数
• 对象没有虚拟成员
• 对象不使用运行时类型识别 (RTTI)
如果不满足这些条件中的任何一个,那么传输指针或引用通常会更快。如果对象很大,那么复制整个对象显然需要时间。此外任何将对象复制到参数时必须调用复制构造函数,并且析构函数(如果有)必须在函数返回之前调用。
将复合对象传递给函数的首选方法是使用 const引用(since C++11 右值引用亦可)。 const 引用确保原始对象未被修改(与const 引用相比,右值引用可以修改原始对象)。不像一个指针或非 const 引用,const 引用允许函数参数是一个表达式或匿名对象。如果函数是内联的,编译器可以很容易地优化掉一个 const引用。
参数传递方式 | 函数参数限制 | 指针/普通引用 | 只允许函数参数是一个varible | const 引用 | 函数参数是一个varible 或一个表达式或匿名对象(临时对象) | 右值引用 | 函数参数是一个右值,如一个表达式或匿名对象(临时对象) | 表1 函数参数传递方式比较
另一种解决方案是使函数成为对象的类或结构的成员。这同样有效。 |
|