结构体和类

结构体和类

面向对象编程使得软件具有模块化和更加清晰的特点,所谓对象就指结构和类的实例
这种编程方法对程序性能有两面影响:
** 正面影响:**

  • 如果一起使用的变量是相同结构或者类的成员,那么他们会被存储到一起,这使得数据缓存效率更高
  • 不需要将类成员和变量作为参数传递给成员函数,避免了参数传递的开销

** 负面影响: **

  • 非静态成员函数有一个this指针,会作为隐形参数传递个函数,this的参数传输开销会在所有的非静态成员函数上产生

  • this指针占用一个寄存器,而寄存器是稀缺资源

  • 虚成员函数的效率比较低

    综合来说,使用类和成员函数的代价并不太大,若面向对象的编程风格利于程序的逻辑结构和清晰性,那么可以使用,只要避免在程序的最关键部分调用过多的函数即可。

    类的数据成员

    类或者结构体的数据成员是按照创建类或者结构实例时声明他们的顺序连续存储,且不存在性能损失,基本上与访问同类型简单变量开销一致

    编译器会将数据成员对齐到可以最大成员变量的整数倍以优化访问,因此可以对变量重新排序,可能会减少结构体的空间占用。

    如果类有至少一个虚成员函数,那么在第一个数据成员或者最后一个成员之后有一个指向虚拟表的指针,在32位系统中是4字节,而在64位系统中是8字节。

    如果成员相对于结构体或者类的开头的偏移量小于128,那么代码会更紧凑,因为偏移量可以表示为8位的有符号数字,如果大于128,则偏移量必须表示为32位数字(8位 32位之间指令集没有其它可选的偏移量)
    例如

    1
    2
    3
    public:
    int a[100];
    int b;

    这里b的偏移量是400,因此访问b的代码需要将偏移量编码为32位整数,这里如果交换a b的顺序,那么就可以用一个8位的偏移量来访问a。从而使代码更近吹,从而更有效的利用代码缓存。因此,在结构体后者类生命中,大数组和其它大对象建议放在最后,最常用的数据成员放在前面,如果不能在前128位中包含所有的数据成员,那么将最常用的放在前128字节中。

类的成员函数(方法)

每次声明或者创建类的新对象时,都会生成数据成员的新实例,但是每个成员函数都只有一份实例,函数代码不会被复制,因为相同的代码可以应用于类的所有实例。

调用类的成员函数也没有额外的开销。

静态成员函数不能够访问任何非静态数据成员或者非静态成员函数,静态成员函数比非静态成员函数快,因为不需要this指针,如果成员函数不需要访问任何非静态的东西,可以将其声明为静态函成员函数已获得更快的速度。

虚成员函数

虚函数用于实现多态类,一个多态类的每一个实例都有一个指针指向虚函数表,运行时通过虚函数表查找虚函数的正确版本
多态性是面向对象程序比非面向对象程序效率低的主要原因之一。

运行时类型识别(RTTI)

运行时类型识别会向所有类对象添加额外的信息,而且效率不高。

继承

子类和父类成员的访问速度相同,继承几乎没有任何性能损失

构造函数和析构函数

构造函数和其它成员函数效率相同,如果对象不需要初始化,那么不需要默认构造函数,如果仅通过复制所有的数据成员就可以复制对象,则不需要复制构造函数。
析构函数和其他成员函数效率也相同,如果没有必要,则不要创建析构函数。

浮点归纳变量

编译器无法优化浮点归纳变量,原因同代数化简,由于精度问题,改变浮点计算方式或顺序可能会产生奇怪的后果。因此我们有必要手动完成这个工作。

内联函数的非内联副本

由于同一个函数可能从另一个模块中调用,为了在另一个模块中调用该函数,编译器必须生成一个内联函数的非内联副本,如果没有其它模块调用该函数,那么这个非内联副本就是无用代码,会降低代码缓存利用率。

可以通过加static关键字限定本文件作用域,同时static也利于编译器做其他优化。

CPU优化的障碍

现代cpu可以通过乱序执行进行很多优化,但代码中的长依赖链会妨碍其乱序执行,因此我们应该避免长依赖链,特别死有长延迟的循环依赖链

编译器优化选项

所有c++编译器都有各种各样的优化选项,而许多优化选项与调试都不兼容,因此通常可以生成两个版本的可执行文件,一个支持调试,和一个带有所有相关优化选项的发布版本

大多数编译器都提供了大小优化和速度优化的选择,代码非常快时,希望可执行文件非常小,或者代码缓存非常关键时,优化大小很重要。当CPU访问和内存访问消耗巨大时,可以选择可用优化程度最大的速度优化选项。

一些编译器提供配置分析引导的优化,可以利用分析器确定程序流以及每个函数和分支执行的次数,然后利用这些信息来优化代码

CPU发展历史中,每一代CPU都增加了可用的指令集,新指令集可以使编译器生成更高效的代码。当不需要兼容旧版本CPU是,可以选择较新的指令集,也可以使代码中最关键的部分有多个版本以支持不同的CPU。

当没有异常处理时,代码会更加高效,建议是关闭对异常处理的支持。

建议关闭对运行时类型识别(RTTI)的支持

建议启用快速浮点运算或者关闭对严格浮点运算的要求。

1
2
3
4
5
6
7
8
#include <xmmintrin.h>
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);


//若SSE2可用
#include <xmmintrin.h>
_mm_setcsr(_mm_getcsr() | 0x8040);

优化指令

适用于所有*C++*编译器的关键字

可以将register关键字添加到变量声明中,以告诉编译器希望他是寄存器变量,register关键字只是一个提示,编译器可能不接受,但是在编译器无法预测哪些变量被最多使用的情况下,他会非常有用。

register相反的是volatilevolatile关键字确保变量永远不会存储在寄存器中,即使是临时的。它适用于多个线程之间共享的变量。

const关键字表示变量永远不会改变,将允许编译器在许多情况下优化掉变量,例如

1
2
3
4
5
6
const int ArraySize = 1000;
int List[ArraySize];

for(int i = 0; i < ArraySize; i ++) {
List[i] ++;
}

上述代码中,编译器能在编译时知道常量的值,将不会为整数常量分配内存,除非其地址被取走。

static关键字有多种含义,当其应用于非成员函数时,限定其作用域,使内联更高效,并支持过程间优化。用于成员函数时,可以避免this指针的开销

特定编译器的关键字

快速函数调用__fastcall 或者 __attribute((fastcall)),可以让函数调用在32位模式下更快。

纯函数__attribute((const)) (Linux Only) 将函数声明为纯函数,利于消除公共子表达式,常量传播以及移动循环不变量

假设没有指针别名。 __declspec(noalias)或者__restrict 或者 #pragma optimize("a", on)

数据对齐。__declspec(align(16)) 或者 __attribute__(aligned(16)),指定结数组和结构体的对齐,这对向量操作非常用用。

检查编译器做了什么

通过查看汇编代码,可以知道,从而进一步做手动优化,这要求我们会看懂汇编代码(还得学)