串行代码优化可分为以下几个层次: 系统级别,应用级别,算法级别,函数级别,循环级别,语句级别,指令级别。
1.系统级别
1.1 网络速度,利用率及负载均衡
如果应用经常等待网络数据的传输和到达,那么就得考虑网络的速度和利用率,如果是集群还得考虑网络的负载均衡。
1.2 处理器利用率
找出处理器利用率较低或较高的原因。
1.3 存储器带宽利用率
(1)提高存储器访问的局部性以增加缓存利用。
(2)将数据保存在临时变量中以减少存储器的读写。
(3)减少读写依赖。
(4)同时读写多个数据以提高系统的指令级并行和向量化能力。
1.4 去阻塞处理器运算的因素
通过观察处理器的使用率来估计处理器有多少比例的运算时间在等待IO操作完成,如果比较接近,则可使用非阻塞的函数调用或独立的线程来处理IO。
2 应用级别
2.1 编译器选项
如GCC有 O0(无优化),O1,O2(常用),O3等优化选项。
2.2 调用高性能库
如BLAS ,FFTW
2.3 去掉全局变量
全局变量尤其是多个文件共享的全局数据结构会阻碍编译器的优化。对于并行程序,全局变量应当绝对禁止。
2.4 受限的指针
指针标识符 restrict
2.5 条件编译
条件编译可使代码更简短,效率更高。
3. 算法级别
3.1 索引顺序
访问多维数据时的局部性直接与各维数据在内存中存放的先后顺序相关,如C语言中数组是以行为主序存放的,在计算时尽量按行访问数据。
优化前的代码:
for(int i = 0; i < N; i++){ for(int j = 0; i < M; j++){ r[j] += a[j][i]; } }
优化后的代码:
for(int i = 0; i < M; i++){ float ret = 0.0f; for(int j = 0; i < N; j++){ ret += a[i][j]; } r[i] = ret; }
3.2 缓存分块
如果数据的大小超过了缓存的大小,那么久容易出现满不命中的情况,此时常见的减弱不命中的代价的方法主要是缓存分块。
以矩阵乘法为例:
for(i =0; i < N; i+= NB) for(j = 0; j <M; j+= NB) for(k = 0; k < K; k+= NB) for(i0 = i; i0 < (i+NB); i0++) for(j0 = j; j0 < (j+NB); j0++) for(k0 = k; k0 < (k+NB); k0++){ c[i0][j0] += A[i0][k0]+B[k0][j0]; }
3.3 软件预取
预取指在数据被使用前,投机地将其加载到缓存中。必须妥善考虑进行的时机和实施强度。
3.4 查表法
通常将查表法和线性插值结合使用,以减少计算精度的降低。
4 函数级别
4.1 函数调用参数
如果函数参数是大结构体,应当通过传指针或引用以减少调用是复制和返回时销毁的开销。
4.2 内联小函数
能够消除函数调用的开销,提供更过指令并行,表达式移除等优化机会,进而增强指令流水线的性能。
5 循环级别
5.1 循环展开
循环展开不仅减少了每次的判断数量和循环变量的计算次数,更能够增加处理器的流水线性能。通常展开小循环且内部没有判断的会获益,展开大循环则可能会因为导致寄存器溢出而导致性能下降,而展开内部有判断的循环会增加反之预测的开销也会导致性能下降。
循环展开前:
float sum = 0.0f; for(int i =0; i < num; i++) sum+= a[i]; }
展开循环后: (展开循环需要注意处理某尾的数据)
float sum 0.0f; sum1 = 0.0f; sum2 = 0.0f; sum3 = 0.0f; for(int i = 0; i < num; i++){ sum1 += a[i]; sum2 += a[i+1]; sum3 ++ a[i+2]; sum +=a[i+3]; } sum += sum1 + sum2 + sum3;
5.2 循环累积
循环累积主要和循环展开同时使用,在减少寄存器的使用量的同时保证平行度。
float sum = 0.0f, sum1 = 0.0f ,sum2 = 0.0f; for(int i = 0; i < num; i += 6;){ sum1 += a[i] + a[i+1]; sum += a[i+2] + a[i+3]; sum2 += a[i+4] + a[i+5]; } sum += sum1 + sum2;
如果直接使用循环展开6次,则总需至少6个临时变量,而现在只要3个,潜在减少了寄存器的使用。
5.3 循环合并
如果多个小循环使用的寄存器数量没有超过处理器的限制,那么合并这几个小循环可能会带来性能好处(增加了乱序执行能力)。 合并的两循环之间没有依赖。
5.4 循环拆分
如果大循环存在寄存器溢出的情况,那么将大循环拆分能提高寄存器的使用率,
6 语句级别