对程序的编写进行优化是最常用、学习成本最低、硬件成本也最低的优化方式,所以我打算从这部分开始写具体的优化方式。以下从算法优化、数据结构优化、函数优化、循环优化、语句优化,五个方面展开论述
目录
1算法优化
2数据结构优化
3函数优化
4循环优化
通常程序消耗时间最长的部分就是循环了,虽然主流的编译器会对循环进行一定程度的优化,但是很多复杂的控制过程还是需要手动修改达到最佳的性能提升效果。
循环不变量外提
循环不变量指的是在本循环范围内不改变的量,不单单是指常量,还可以指在本次循环中结果不变的表达式。
优化前
for (i = 0; i < 5; i++) {
for (j = 0; j < 3; j++) {
sum += (j + 1)*array[i] * constant;
}
}
优化后
for (i = 0; i < n; i++) {
// 将循环内的计算移到循环外部
int temp = array[i] * constant; // 计算循环外的不变量
for (j = 0; j < m; j++) {
sum += temp * (j + 1);
}
}
循环展开和压紧
将循环体内的代码复制多次执行,可以将迭代间并行转为迭代内并行,还可以发掘出数据级并行,利用向量运算等形式提高性能。
优化前
for (i = 0; i < 10; i++) {
sum += i * i + i - 1;
}
优化后
for (i = 0; i < 10; i += 2) {
sum += i * i + i - 1;
sum += (i + 1) * (i + 1) + i;
}
循环压紧并非是循环展开的逆运算,而是进一步将原本复制的循环语句转化为更紧凑的步骤执行,减少其中的冗余运算,比如向量化运算就是。
循环合并
循环合并是指将具有相同迭代空间的两个循环合并为一个循环。但是要注意好逻辑关系,有的循环关系是不能合并的。
优化前
// 第一个循环
for (i = 0; i < 10; i++) {
sum1 += array[i] * i;
}
// 第二个循环
for (i = 0; i < 10; i++) {
sum2 += array[i] * (i + 1);
}
优化后
for (i = 0; i < 10; i++) {
sum1 += array[i] * i;
sum2 += array[i] * (i + 1);
}
错误的合并
//合并前
for(int i=0; i<N; i++)
A[i] = B[i] + C;
for(int i=0; i<N; i++)
D[i] = A[i+1] + E;
//合并后
for(int i=0; i<N; i++){
A[i] = B[i] + C;
D[i] = A[i+1] + E;
}
循环分段
将单层循环变为双层循环,本身没啥,但有助于后续优化。
优化前
for(int i=0; i<128; i++){
A[i] = B[i] + C[i];
}
优化后
int k=32;
for(int i=0; i<128; i+=k){
for(int j=i; j<i+k-1; j++){
A[j] = B[j]+C[j];
}
}
循环交换
当两个嵌套的循环之间没有另外的语句时,称其为紧嵌套循环。有时候改变两个循环的内外关系,可以利用局部性原理对程序进行优化。
优化前
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
A[j][i] = B[j][i] + C[j][i];
}
}
优化后
for(int j=0; j<N; j++){
for(int i=0; i<N; i++){
A[j][i] = B[j][i] + C[j][i];
}
}
循环倾斜
讲起来比较麻烦,看图吧。
如图一所示,在对该二维数组进行运算时,内循环存在明显的依赖关系,所以内部循环无法并行执行(代码如下所示)。
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
A[i][j] = 1;
}
}
for(int i=1; i<N; i++){
for(int j=1; j<N; j++){
A[i][j] = A[i-1][j] + A[i][j-1];
}
}
但是,当我们将该二维数组倾斜,可以看到,每一列之间是可以并行执行的,所以可以利用如下代码加强它的并行性。
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
A[i][j] = 1;
}
}
for(int j=2; j<2*N; j++){
for(int i=max(1,j-N); i<min(N,j); i++){
A[i][j-1] = A[i-1][j-i] + A[i][j-i-1];
}
}
5语句优化
删除冗余语句
由于多次优化等问题,程序中可能存在死代码,即除了初始化之后再没有调用过的变量,或者永远不会执行的语句。将它们删除后能够减少运行的时空间。
代数变换
将复杂的、执行步骤多的代数过程,替换为等价的简单的、执行步骤少的代数过程。这也可以理解为程序员人脑预计算的过程。
优化前
a = (a+a)+(6*a)/2;
优化后
a = 5*a;
去除相关性
如果两个语句具有相关性,即先执行完A才能执行B,那么编译器就不能自动调整语序、也不能进行向量化等操作。所以,为了编译器的进一步优化,应该手动避免语句具有相关性。
相关性分为数据依赖关系和控制依赖关系。
数据依赖关系指的是两个语句访问的是同一个变量,并且其中至少有一个是写操作(修改了变量值)。
依赖关系 | 举例 | 消除方式 |
---|---|---|
真依赖 | a=1; xxx; b=a; | 无 |
输出依赖 | a=1; xxx; a=2; | 重命名a1=1; xxx; a2=2; |
反依赖 | a=b; xxx; b=2; | 重命名a=b1; xxx; b2=2; |
控制依赖关系指的是a语句是b语句的条件语句,所以b不能先于a进行。
if(a){
b;
}
以下是3种优化方法:
1.标量扩展
标量指的是只含有一个值的变量。标量扩展指的是将原本循环中需要一个标量的语句用一个数组来代替,从而能进行进一步地并行化。
优化前
#include <stdio.h>
int main() {
int a[4] = {1, 2, 3, 4};
int b[4] = {5, 6, 7, 8};
int c;
for (int i = 0; i < 4; ++i) {
c = a[i] + b[i];
printf("%d ", c);
}
printf("\n");
return 0;
}
优化后
#include <stdio.h>
void vector_add(int *a, int *b, int *c, int n) {
for (int i = 0; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
int main() {
int a[4] = {1, 2, 3, 4};
int b[4] = {5, 6, 7, 8};
int c[4];
vector_add(a, b, c, 4);
for (int i = 0; i < 4; ++i) {
printf("%d ", c[i]);
}
printf("\n");
return 0;
}
更进一步向量化优化
#include <stdio.h>
#include <immintrin.h> // 包含 AVX2 指令集的头文件
void vector_add(int *a, int *b, int *c, int n) {
int i;
__m256i va, vb, vc;
for (i = 0; i < n; i += 8) {
// 加载 8 个整数到 AVX 寄存器中
va = _mm256_loadu_si256((__m256i*)&a[i]);
vb = _mm256_loadu_si256((__m256i*)&b[i]);
// 执行加法操作
vc = _mm256_add_epi32(va, vb);
// 将结果存储回内存
_mm256_storeu_si256((__m256i*)&c[i], vc);
}
}
int main() {
int a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
int b[8] = {9, 10, 11, 12, 13, 14, 15, 16};
int c[8];
vector_add(a, b, c, 8);
for (int i = 0; i < 8; ++i) {
printf("%d ", c[i]);
}
printf("\n");
return 0;
}
2.标量重命名
也即上述表中输出依赖和反依赖的优化方法,此处就不举例了。
3.数组重命名
其实类似于标量重命名。在循环中,可能多个数组的运算之间存在依赖关系,通过引入中间临时数组就可以消除该依赖。
优化前
for(int i=1; i<N; i++){
A[i] = A[i-1] + 1;
B[i] = A[i] + 2;
A[i] = B[i] + 3;
}
优化后
for(int i=1; i<N; i++){
A1[i] = A[i-1] + 1;//临时数组A1
B[i] = A1[i] + 2;
A[i] = B[i] + 3;
}
公共子表达式优化
当程序中的表达式含有多个相同的子表达式的时候,仅需要计算一次即可。(提取公因式嘛,也可以理解为局部地预计算以空间换时间)
优化前
if((a+b)>3 && (a+b)<5){
a = a+b;
}
优化后
temp = a+b;
if(temp>3 && temp<5){
a = temp;
}
分支语句优化
1.简化判断条件。
优化前
if((a1!=0) && (a2!=0) && (a3!=0)){
xxx;
}
优化后
if(a1 && a2 &&a3){
xxx;
}
2.生成选择指令
运用三目运算指令替代部分选择指令。
优化前
if(a>0){
x=a;
}
else{
x=b;
}
优化后
x = (a>b)?a:b;
3.运用条件编译
利用#if #ifdef #idndef #elif #endif等语句进行编译优化。
#include <stdio.h>
#define OPTION_A_ENABLED 1
int main() {
#if OPTION_A_ENABLED
printf("Option A is enabled.\n");
#else
printf("Option A is disabled.\n");
#endif
return 0;
}
专栏安排(已有,或将有)
一、程序性能优化的意义
五、程序编写时的优化(上):算法优化、数据结构优化、函数优化
七、编译与运行时的优化(上):编译器结构、编译选项、编译优化
七、编译与运行时的优化(下):数学库优化、运行时的优化
八、系统配置的优化
九、单核优化
十、OpenMP程序优化
十一、MPI程序优化
十二、…
如有不足之处,敬请批评指正
更欢迎在评论区留下你的见解,你的方法,如果有效我会增加在文章中,并@你。