示例程序
private long mSum;
long combine1(long[] v){
long i;
for(i = 0; i < getLength(v) ; i++){
long val = getElement(v,i);
mSum = mSum + val;
}
return mSum;
}
int getLenght(long[] v){
return v.length;
}
int getElement(long[] v,long index){
if(index < 0 || index >= v.length){
return 0;
}
return v[index];
}
优化一:消除循环的低效率
在示例的程序中,每次循环都需要测试i < getLength(v)这个条件,而这个值是固定的,没有必要每次判断该条件的时候都计算一次该值。
优化二:减少过程调用
在示例程序中,循环中每次都要调用getElement()方法,但这个方法可以不用,将方法中的代码写到循环体中,从而减少过程的调用所带来的开销,此外在getElement()方法中都在进行边界的检测,而在当前的循环调用中每次的角标i都是合法的,减少过程调用也可以避免边界检测所带来的开销。
优化三:消除不必要的内存引用
在示例程序中,每次的循环操作都需要调用mSum的值,每次调用mSum都需要从堆内存中读取和写入该值,可以使用局部变量来代替mSum,只需将最后得到的结果赋值给mSum即可,这种优化能够大大的提高程序的性能。
简单优化后的示例程序
private long mSum;
void combine2(long[] v){
long i;
int len = v.length;
long sum = 0;
for(i = 0; i < len ; i++){
sum += v[i];
}
mSum = sum;
}
简单优化前后的性能对比
- 整数加法:优化前的每元素的周期数(CPE)为22.68,优化后的为1.27
- 整数乘法:优化前的CPE为20.02,优化后为3.01
- 浮点加法:优化前的CPE为19.98,优化后为3.01
- 浮点乘法:优化前的CPE为20.18,优化后为5.01
优化四:循环展开
循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。首先,他减少了不直接有助于程序结果的操作数量,例如循环索引计算和条件分支;第二,他提供了一些方法,可以进一步优化代码,减少整个计算中关键路径上的操作数量。示例代码如下(每次循环做两次的求和):
private long mSum;
void combine3(long[] v){
int i;
int len = v.length;
int limit = len - 1;
long sum = 0;
for(i = 0; i < limit ; i+=2){
sum = (sum + v[i]) + v[i+1];
}
//如果len为奇数,需再执行一次运算
for(; i < len ;i++){
sum += v[i];
}
mSum = sum;
}
此种优化方法主要针对整数的加法运算,对于整数乘法、浮点加法和浮点乘法的效果不大,因为在之前的优化中,只有整数加法的CPE未达到延迟界限,而其他三种运算均已到达,而这种优化方式无法打破延迟界限。
延迟、发射时间和容量
- 延迟:表示完成运算搜需要的总时间
- 发射时间:表示两个连续的同类型的运算之间徐涛的最小时钟周期数
- 容量:表示能够执行该运算的功能单元的数量
- 延迟界限:简单理解就是延迟,整数加法的延迟界限为1,整数乘法的为3,浮点加法为3,浮点乘法为5
- 吞吐量界限:即吞吐量,指发射时间除以容量的值,整数加法为0.5,整数乘法为1,浮点加法为1,浮点乘法为0.5
优化五:提高并行性
方式一:多个累积变量
对于一个可结合和可变换的运算来说,可以通过将一组合并运算分割成两个或者更多的部分,并在最后合并结果来提高性能,示例代码如下:(分割成两个运算)
private long mSum;
void combine4(long[] v){
int i;
int len = v.length;
int limit = len - 1;
long sum1 = 0;
long sum2 = 0;
for(i = 0; i < limit ; i+=2){
sum1 += v[i];
sum2 += v[i+1];
}
//如果len为奇数,需再执行一次运算
for(; i < len ;i++){
sum1 += v[i];
}
mSum = sum1 + sum2;
}
方式二:重新结合变换
通过改变运算的顺序来对性能进行优化,以combine3()中的代码为例:
private long mSum;
void combine5(long[] v){
int i;
int len = v.length;
int limit = len - 1;
long sum = 0;
for(i = 0; i < limit ; i+=2){
sum = sum + (v[i] + v[i+1]);
}
//如果len为奇数,需再执行一次运算
for(; i < len ;i++){
sum += v[i];
}
mSum = sum;
}
不难发现,同combine3相比,唯一的区别在于执行sum的运算时先求出v[i] + v[i+1]的值,然后再将结果同sum运算。
并行性性能总结
通过提高并行性,即便是2×的优化,也能使代码性能在原来的基础上提高两倍左右的性能(除了整数加法,因为在这个过程中其他非循环的操作影响了性能),毫无疑问都打破了延迟界限所带来的限制。