对程序的编写进行优化是最常用、学习成本最低、硬件成本也最低的优化方式,所以我打算从这部分开始写具体的优化方式。以下从算法优化、数据结构优化、函数优化、循环优化、语句优化,五个方面展开论述
目录
1算法优化
| 算法是程序的道,具体实现方式是程序的术,战略上的不足无法用战术来弥补。
1.1选择算法
在对具体问题进行编程进行优化时,必须考虑到所采用的算法是否有更好的版本。比如,排序问题:快速排序、桶排序之于冒泡排序;字符串匹配问题:KMP算法之于暴力匹配;有序搜索问题:二分查找之于暴力查找。
算法是否好,是基于程序优化的目的来说的(按照程序优化的流程,修改程序前必须要确定优化目的,详见《三、程序性能优化流程》)。比如,有的算法消耗时间少消耗空间大,有的则相反,有的在两者之间形成了时空的折中。
| 反者道之动,很多事情的终点都是一个优美的平衡。
1.2改进算法
从算法的过程出发。
你可以通过分析算法的执行过程来改进它。你可以观察算法在不同情况下的表现,找出其中的瓶颈和不足之处。你可能会发现一些冗余的步骤、效率低下的部分或者可以优化的地方。通过深入理解算法的运行原理,你可以提出改进的想法,例如重新设计某些步骤、引入更有效的数据结构或者优化算法的逻辑。
比如,在寻找一个数组具有最大和的子数组时,你原本的代码使用了三个循环来对每个子数组进行求和,后来你发现很多的求和步骤是冗余的,可以将中间过程存储下来,减少计算量。这就是从算法的过程出发。
从算法的编码出发。你可以通过修改算法的实现代码来改进它。相当于是完全改变了算法的过程,而不是在原基础上进行修改。
同样,在寻找一个数组具有最大和的子数组时,你转而使用分治法,或是线性规划的方法,大大提升了算法的效率。但是这样的方法只能另起炉灶找出,而不是在原有的算法上修改得到的。
2数据结构优化
程序=数据结构+算法。可见,数据结构的构造,对于程序的效率也是至关重要。
数据结构的分类
数据结构可以按照逻辑结构和存储结构进行分类。逻辑结构指的是元素之间的关系,比如线性结构、树结构、图结构、集合结构。逻辑结构和算法的设计过程紧密相关,影响的是算法的道。存储结构是指实际元素之间存储位置的关系,有顺序存储和非顺序存储两种,比如同样是线性结构,顺序存储的就是列表(数组),非顺序存储的就是链表。存储结构和算法的实现过程紧密相关,影响的是算法的术(有时候术优化的效果比道还好)。
| 大处着眼,小处着手
典型数据结构的对比
以下使用两个表格,分别对比了经典数据结构的优缺点和基本操作性能。
不同数据结构的优缺点
数据结构 | 优点 | 缺点 |
---|---|---|
数组 | 插入快 | 查找慢、删除慢、大小固定 |
有序数组 | 查找快 | 插入慢、删除慢、大小固定 |
哈希表 | 查找非常快、插入删除非常快 | 存储空间使用率低、很少碰撞运行快速的哈希算法很难设计 |
链表 | 插入删除快、大小灵活 | 查找慢 |
栈 | 后进先出适用于多种需要递归的场景 | 大小固定、查找插入删除慢 |
队列 | 先进先出保证了数据的顺序性 | 大小固定、查找插入删除慢 |
堆 | 动态分配内存 | 查找慢 |
二叉树 | 插入、查找、删除快 | 对数据顺序敏感 |
图 | 可以实现很多现实问题 | 存储和操作开销大 |
不同数据结构的操作性能对比
数据结构 | 查找 | 插入 | 删除 | 遍历 |
---|---|---|---|---|
数组 | O(N) | O(1) | O(N) | |
有序数组 | O(log N) | O(N) | O(N) | O(N) |
链表 | O(N) | O(1) | O(N) | |
有序链表 | O(N) | O(N) | O(N) | O(N) |
二叉树 | O(log N) | O(log N) | O(log N) | O(N) |
二叉树(最坏) | O(N) | O(N) | O(N) | O(N) |
平衡树 | O(log N) | O(log N) | O(log N) | O(N) |
哈希表 | O(1) | O(1) | O(1) |
优化你的数据结构
0.选择合适的数据结构
1.使用更小的单位元素
| 若无必要,勿增实体
尽可能地使用能够满足情形的最小数据结构吧(能用short不用int)。第一、更小的数据结构意味着能有更多的数据保存在缓存中,访问会更加迅速;第二、在使用向量指令进行优化时(《九、单核优化》将会介绍),相比于大尺寸的数据结构,向量寄存器能够存储多的小尺寸数据结构,从而平行度更大;第三、在内存资源有限或者需要大规模数据存储时,使用更小的数据结构可以有效节省内存资源,降低系统的内存消耗,提高系统的性能和稳定性;第四、存储空间更小的数据结构通常意味着数据量更小,因此在数据传输过程中,可以减少数据量,加快数据传输的速度。
2.使用适合硬件结构的数据结构
并行数组是一种将多个数组合并成一个的数据结构,适合硬件结构的原因在于其对于向量化指令集(如SIMD)的友好性。在并行数组中,多个数组中相同索引位置的元素被合并成一个向量,这样可以利用向量化指令一次性操作多个元素,充分发挥处理器的并行计算能力。
3.提高数据访问局部性
3函数优化
别名消除
别名指的是两个指针指向相同的内存区域,此时编译器不能认识到这一点,所以处理的时候几乎不会自动优化。以下是几种别名消除的方法:
-
基于指针分析的别名消除
// 不优化的写法 int a = 5; int* ptr1 = &a; int* ptr2 = &a; *ptr1 = 10; // 修改通过 ptr1 访问的内存区域 *ptr2 = 15; // 修改通过 ptr2 访问的内存区域,此时编译器无法确定 ptr1 和 ptr2 是否引用同一内存区域 // 优化后的写法(别名消除) int a = 5; int* ptr = &a; // 将 ptr1 和 ptr2 合并为一个指针 *ptr = 10; // 修改通过 ptr 访问的内存区域 *ptr = 15; // 同样修改通过 ptr 访问的内存区域,消除了别名关系
-
基于静态分析的别名消除
// 不优化的写法 int a, b; int* ptr = &a; b = *ptr; // 读取通过 ptr 访问的内存区域 // 优化后的写法(别名消除) int a, b; int* ptr = &a; b = a; // 直接将 a 的值赋给 b,消除了通过 ptr 的间接访问
-
基于程序转换的别名消除
// 不优化的写法 void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } void foo() { int x, y; int* ptr = &x; swap(ptr, &y); // 调用 swap 函数交换 x 和 y 的值 } // 优化后的写法(别名消除) void foo_optimized() { int x, y; int temp = x; // 直接使用局部变量 temp,避免了通过指针的间接访问 x = y; y = temp; }
常数传播
简而言之就是让程序少做无用的,提前可以避免的运算、变量带值。这虽然看起来影响很熹微,但是在大循环中很有效。
举几个例子。
优化前
x = 10 + 5
y = x * 2
优化后
x = 15
y = x * 2
优化前
const PI = 3.14
radius = 5
area = PI * radius * radius
优化后
const PI = 3.14
radius = 5
area = 3.14 * radius * radius
虽然很简单,但是说真的很有效果,利用这个方法可以将许多后量子密码标准的源代码提升40%左右的速度。
传参优化
传递媒介有寄存器和栈两种,前者比后者快接近十倍,所以我们当然要尽可能地让寄存器去传递参数。有以下两种方法:
-
传递的参数尽可能存储空间小,个数尽可能少
-
对于存储空间较大,或是个数较多的参数,使用结构体存储这些参数,传参时只传递结构体的指针
内联替换
如果说常数传播优化是为了避免常数参数的取值操作,那么内联替换就是为了避免简单函数被调用产生时空开销。可以使用以下方式进行优化:
-
手动内联:程序员手动将函数调用替换为函数体,通常用于短小的函数或者需要频繁调用的函数。
// 原始函数 inline int add(int a, int b) { return a + b; } // 手动内联 int result = 3 + 4;
-
编译器自动内联: 编译器根据一些启发式规则自动决定哪些函数适合内联。
// 原始函数 inline int square(int x) { return x * x; } // 函数调用 int result = square(5);
-
内联缓存:将函数的返回值缓存起来,以避免重复计算。
// 原始函数 inline int square(int x) { return x * x; } // 内联缓存 int result = square(5); result = square(6); // 在一些情况下,编译器可能会将上一次调用的结果缓存起来,避免重复计算
全局变量优化
全局变量的使用会影响编译器的自动优化,所以可以通过以下方式来优化:
- 将全局变量改为局部变量使用(全局变量会被改变的情况)
- 将全局变量的值直接写,也就是常数传播(全局变量不被改变的情况)
4循环优化(好累,前面的区域以后再来探索吧)
5语句优化(好累,前面的区域以后再来探索吧)
专栏安排(已有,或将有)
一、程序性能优化的意义
五、程序编写时的优化(上):算法优化、数据结构优化、函数优化
七、编译与运行时的优化(上):编译器结构、编译选项、编译优化
七、编译与运行时的优化(下):数学库优化、运行时的优化
八、系统配置的优化
九、单核优化
十、OpenMP程序优化
十一、MPI程序优化
十二、…
如有不足之处,敬请批评指正
更欢迎在评论区留下你的见解,你的方法,如果有效我会增加在文章中,并@你。