优化性能基本策略
- 高级设计:选择合适的算法和数据结构
- 基本编码原则:编写出编译器能够有效优化以转换成高效可执行代码的源代码
- 消除连续的函数调用。在可能时,将计算移到循环外
- 消除不必要的存储器引用。引用临时变量保存中间结果,最后将计算的值写回
- 低级优化:降低开销
- 循环展开
- 多个累积变量和重新结合的方法,提高指令级并行
- 用功能的风格充血条件操作,使编译采用条件数据传送
- 并行编程:针对处理运算量特别大的计算,将一个任务分成多个部分,这些部分可以在多核核多处理器的某种组合上并行计算
编译器优化
GCC优化级别
- 基本优化:命令行标志
-O1
- 全面优化:命令行标志
-O2
- 全面优化:命令行标志
-O3
局限
编译器只会小心地对程序使用安全的优化
程序的性能
度量标准
每元素的周期数(CPE):帮助我们在更详细的级别上理解迭代程序的循环性能
记录元素个数和所用的周期,使用最小二乘方法拟合得到时钟周期和元素个数的一次函数关系,系数即为每元素的周期数(CPE)
优化方法
实验加上分析:反复尝试不同的方法,进行测量,并检查会变代码表示以确定底层的性能瓶颈
现代微处理器结构
功能单元性能
利用延迟和发射时间描述功能单元性能
- 延迟:完成运算所需要的总时间
- 发射时间:两个连续同类型运算之间需要的最小时钟周期数
- 最大吞吐量:发射时间的倒数
Intel Core i7 运算 | 整数延迟 | 整数发射 | 单精度延迟 | 单精度发射 | 双精度延迟 | 双精度发射 |
---|---|---|---|---|---|---|
加法 | 1 | 0.33 | 3 | 1 | 3 | 1 |
乘法 | 3 | 1 | 4 | 1 | 5 | 1 |
除法 | 11~21 | 5~13 | 10~15 | 6~11 | 10~23 | 6~19 |
- 随着字长的增加,更复杂的数据类型,更复杂的运算,延迟会增加
- 大多数形式的加法和乘法运算发射时间为1,这是通过流水线实现的。
- 发射时间为1的功能单元被称为完全流水线化的:每个时钟周期可以开始一个新的运算
- 典型的浮点加法器包括三个阶段(三周期延迟)
- 处理指数值
- 小数相加
- 进行舍入
- 整数加法的发射时间为0.33,因为硬件由3个完全流水线化的能够执行整数加法的功能单元
- 除法器不是完全流水线化的,发射时间只比延迟少几个周期。除法的延迟和发射时间是以范围的形式给出的,因为某些被除数和除数的组合比起他组合需要更多的步骤。除法的长延迟和长发射时间是一个相对开销很大的运算
- 微处理器芯片需要平衡有限的空间和性能,所以整数加法、整数乘法、浮点加法、浮点乘法采用了大量的硬件
程序最大性能
利用延迟界限和吞吐量界限描述程序最大性能
- 延迟界限:代码中数据相关限制了处理器利用指令级并行的能力,给出了任何必须按照严格顺序完成合并运算的函数所需的最小CPE值
- 吞吐量界限:刻画了处理器功能单元的原始计算能力,这个界限是程序性能的终极限制,给出了CPE的最小界限
Intel Core i7 运算 | 整数延迟界限 | 整数吞吐量界限 | 单精度延迟界限 | 单精度吞吐量界限 | 双精度延迟界限 | 双精度吞吐量界限 |
---|---|---|---|---|---|---|
加法 | 1 | 1 | 3 | 1 | 3 | 1 |
乘法 | 3 | 1 | 4 | 1 | 5 | 1 |
- 处理器由3个能够进行整数加法的功能单元,所以整数加法的发射时间为0.33,但是因为需要从存储器读数据,造成了合并函数CPE为1的另一个吞吐量界限
处理器操作抽象模型
数据流:使用图形化的表示方法展示不同操作之间的数据相关是如何限制它们的执行顺序,这种限制成了图中的关键路径,这是执行一组机器指令所需要的周期数的一个下界
四类寄存器
- 只读:只用作源值,可以作为数据,也可以用来计算存储器地址,但是不会被修改。本段代码中是
%rax
,%rbp
- 只写:作为数据传送操作的目的。本段代码无
- 局部:在循环内部被修改和使用,迭代与迭代之间不相关。本段代码中条件寄存器
- 循环:既作源值,又作目的,一次迭代中产生的值会在另一次迭代中用到。本段代码中
%rdx
,%xmm0
其中循环寄存器之间的操作链决定了限制性能的数据相关
将数据流图进一步改进,得到新的数据流图,再进一步只保留循环寄存器
循环 n n 次迭代计算的数据流
左边的链需要个周期,右边的链需要 n n 个周期,因此左边的链成为关键路径,CPE为4
但是整数加法的CPE为2,而数据流图的左边的链和右边的链得到的CPE为1,说明一个原则:数据流表示中的关键路径提供的知识程序需要周期数的下界
其它性能因素
- 可用功能单元的数量
- 功能单元之间能够传递数据值的数量
通用优化策略
消除循环的低效率
避免在循环的测试条件中进行函数调用或者引用,如果循环迭代时的测试条件不会改变,将计算移动到代码前面不会被多次求值的部分,称为代码移动
减少过程调用
比如在保证安全的情况下减少边界检查
减少不必要的存储器引用
对于经常使用获取指针指向变量,使用临时变量来累计计算,计算完成后结果存放到指针指向的位置,减少无用的存储器读写
利用微体系结构的优化策略
循环展开
通过增加每次迭代计算的元素的数量,减少循环的迭代次数
从两个方面提高程序性能。首先,减少了不直接有助于程序结构的操作的数量,例如循环索引计算和条件分支。其次,提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量
用命令行选项
-funroll-loops
或者优化级别足够高,调用GCC会执行循环展开提高并行性
利用功能单元流水线化能力,减少合并操作的延迟
多个累积变量
对于一个可结合和可交换的合并运算来说,将合并运算分割成两个或更多的部分,并在最后合并结果来提高性能
引入并行形式的编译器相对较少
重新结合变换
改变合并执行的方式,也极大地提高程序的性能
大多数编译器不会尝试重新结合变换
使用SIMD指令
SSE指令(SIMD扩展)一次从存储器独处多个值,并行执行计算
用命令行选项
-msse4
来使用SSE指令
限制因素
寄存器溢出
如果并行度超过了可用的寄存器数量,需要将某些临时值存放到栈中,一旦出现这种情况,性能会急剧下降
分支预测和预测错误处罚
分支预测并且预测错误,处理器必须丢弃掉所有投机执行的结果,在正确的位置重新开始取指令
通用原则
不要过分关心可预测的分支
现代处理器的分支预测逻辑非常善于辨别不同的分支指令有规律的模式和长期的趋势
书写适合用条件传送实现的代码
使用功能式的风格实现函数,例如三元运算
? :
程序剖析和优化
程序剖析
编译时添加命令行选项
-pg
unix> gcc -O1 -pg prog.c -o prog
执行程序
unix> ./prog file.txt
调用
GPROF
来分析gmon.out
中的数据unix> gprof prog
第一部分列出了执行各个函数花费的时间
第二部分列出了函数的调用历史
Amdahl定律
当加快系统一个部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少
考虑一个系统,执行某个应用程序需要时间 Told T o l d ,某个部分需要这个时间的百分比为 α α ,而我们将它的性能提高到了 k k 倍,因此新的执行时间会是
因此加速比
S=Told/Tnew=1(1−α)+α/k S = T o l d / T n e w = 1 ( 1 − α ) + α / k
参考代码
结合问题
/* Create abstract data type for vector */ typedef struct { long int len; data_t *data; } vec_rec, *vec_ptr; /* Create vector of specified length */ vec_ptr new_vec(long int len) { /* Allocate header structure */ vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec)); if (!result) return NULL; /* Couldn’t allocate storage */ result->len = len; /* Allocate array */ if(len>0){ data_t *data = (data_t *)calloc(len, sizeof(data_t)); if (!data) { free((void *) result); return NULL; /* Couldn’t allocate storage */ } result->data = data; } else result->data = NULL; return result; } /* * Retrieve vector element and store at dest. * Return 0 (out of bounds) or 1 (successful) */ int get_vec_element(vec_ptr v, long int index, data_t *dest){ if (index < 0 || index >= v->len) return 0; *dest = v->data[index]; return 1; } /* Return length of vector */ long int vec_length(vec_ptr v) { return v->len; } /* Implementation with maximum use of data abstraction */ void combine1(vec_ptr v, data_t *dest){ long int i; *dest = IDENT; for (i = 0; i < vec_length(v); i++) { data_t val; get_vec_element(v, i, &val); *dest = *dest OP val; } } /* Move call to vec_length out of loop */ void combine2(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); *dest = IDENT; for (i = 0; i < length; i++) { data_t val; get_vec_element(v, i, &val); *dest = *dest OP val; } } data_t *get_vec_start(vec_ptr v) { return v->data; } /* Direct access to vector data */ void combine3(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); data_t *data = get_vec_start(v); *dest = IDENT; for(i=0;i<length;i++){ *dest = *dest OP data[i]; } } /* Accumulate result in local variable */ void combine4(vec_ptr v, data_t *dest){ long int i; long int length = vec_length(v); data_t *data = get_vec_start(v); data_t acc = IDENT; for (i = 0; i < length; i++) { acc = acc OP data[i]; } *dest = acc; } /* Include bounds check in loop */ void combine4b(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); data_t acc = IDENT; for(i=0;i<length;i++){ if (i >= 0 && i < v->len) { acc = acc OP v->data[i]; } } *dest = acc; } /* Unroll loop by 2 */ void combine5(vec_ptr v, data_t *dest){ long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for(i=0;i<limit;i+=2){ acc = (acc OP data[i]) OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; } /* Unroll loop by 2, 2-way parallelism */ void combine6(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc0 = IDENT; data_t acc1 = IDENT; /* Combine 2 elements at a time */ for(i=0;i<limit;i+=2){ acc0 = acc0 OP data[i]; acc1 = acc1 OP data[i+1]; } /* Finish any remaining elements */ for (; i < length; i++) { acc0 = acc0 OP data[i]; } *dest = acc0 OP acc1; } /* Change associativity of combining operation */ void combine7(vec_ptr v, data_t *dest) { long int i; long int length = vec_length(v); long int limit = length-1; data_t *data = get_vec_start(v); data_t acc = IDENT; /* Combine 2 elements at a time */ for(i=0;i<limit;i+=2){ acc = acc OP (data[i] OP data[i+1]); } /* Finish any remaining elements */ for (; i < length; i++) { acc = acc OP data[i]; } *dest = acc; }
函数 方法 整数加法 整数乘法 浮点数加法 单精度浮点数乘法 双精度浮点数乘法 combine1 抽象的未优化 29.02 29.21 27.40 27.90 27.36 combine1 抽象的-O1 12.00 12.00 12.00 12.01 13.00 combine2 移动vec_length 8.03 8.09 10.09 11.09 12.08 combine3 直接访问data[i] 6.01 8.01 10.01 11.01 12.02 combine3 -O1 6.01 8.01 10.01 11.01 12.02 combine3 -O2 3.00 3.00 3.00 4.00 5.00 combine4 结果累积在临时变量中 2.00 3.00 3.00 4.00 5.00 combine4 结果累积在临时变量中,冗余的边界检查 4.00 4.00 4.00 4.00 5.00 combine5 循环展开2次 2.00 1.50 3.00 4.00 5.00 combine5 循环展开3次 1.00 1.00 3.00 4.00 5.00 combine6 循环展开2次,结果2路并行 1.50 1.50 1.50 2.00 2.50 combine7 循环展开2次,data[i]重新结合 2.00 1.51 1.50 2.00 2.97