科技网

当前位置: 首页 >IT

程序设计性能多层优化

来源: 作者: 2018-08-10 20:54:26

程序设计性能多层优化

作者: 王豪迈

想要和作者还有《大话存储》作者冬瓜哥、《PCI Express体系结构导读》作者王齐、《蛋蛋读NVMe》作者蛋蛋等全世界的大牛讨论SSD及存储相关技术?加nanoarch为好友,拉你进ssdfans群。

?欢迎给ssdfans投稿,投稿就能加入ssdfans作者群,和冬瓜哥,蛋蛋等大咖切磋武艺,还有稿酬拿.

作者王豪迈目前是XSKY CTO,为Ceph核心开发者,为Ceph贡献了4%的代码。

高层设计:

选择合适的数据结构和算法,并且在要避免实现算法和编码时引入可能影响性能的问题。

如:在实现快速排序时,避免错误的编码使得时间复杂度变成O(n2)。正确的实现自己所需的数据结构和算法非常重要。

基本编程原则:

编码时避免晦涩的优化:因为现代编译器会很好的产生高效的代码。如,临时变量的使用,有时候,程序员试图减少临时变量的使用来避免不必要的多余变量,但在现代编译器中,如GCC中,一些整数变量会直接用常量代替,多余的变量编译器也会自动省去,或者等到用到时再提取。不必要的优化,省略使得代码晦涩,难读。同时也没有增加程序性能。

消除过多的函数调用:

考虑以下函数

1

2

3

for ( int i=0; i strlen (s); ++i) {

d[i] = s[i];

}

在这个循环中,s没有发生变化,因此strlen(s)是一个常数,完全可以提前到循环前,每次循环都要判断大大影响了程序的性能。有些同学可能认为编译器会识别到此类情况,自动将strlen(s)替换为常量。

但是,编译器优化的核心原则就是:避免一切可能改变程序运行的优化。在循环中,可能会有改变s的代码,因此编译器不会试图替代strlen(s),这是一个危险的行为

程序设计性能多层优化

还有使用内联函数来替代,不断的进入函数和退出函数会多出很多指令来。

3. 消除不必要的内存引用:

使用局部变量来替代引用内存中数据。考虑以下函数:

1

2

3

4

5

6

void sum( int array[], int length, int *sum) {

int i;

for (i=0; i length; ++i) {

*sum += array[i];

}

}

如果查看这个函数的汇编代码,会发现,每次循环都会先将sum赋值给寄存器变量,然后再对寄存器变量做和运算,再把寄存器变量赋给sum,如此的话,每次循环都会有不必要的两次赋值运算。直接使用

1

2

3

4

5

6

7

void sum( int array[], int length, int *sum) {

int i, s;

for (i=0; i length; ++i) {

s += array[i];

}

*sum = s;

}

可以避免循环中不必要的引用内存,大大加快程序运行时间。

底层优化:

解开循环来减少循环上限并且能提高性能:

在求和函数中,

1

2

3

4

5

6

7

for ( int i=0; i length; ++i) {

sum += array[i];

}

for ( int i=0; i length; i+=2) {

sum += array[i];

sum += array[i+1];

}

以上两个循环对比,下面这个循环可以提高运行性能,主要是因为在现代处理器中,指令可以达到并行化和流水线化,一个汇编指令在处理器中都被分解为多条处理器指令,如load, store等,每个处理器中都具有多个运算单元,但是由于循环中需要不断比较ilength,处理器无法明确下一条指令,在运算单元求和时,其他处理器单元就会停止工作。在解开循环后,在多个时钟周期中,可以让其他部件也执行指令,也就是说,在运行一条指令的时间里,其实现代处理器完全可以几乎同时运行其他处理器指令(下面解释),如提取内存中的array[i+1]。解开循环可以大大加快运行时间。另外,解开更多的循环可以不断的提高性能。

2. 为了增加指令级别的并行处理能力,可以使用多个运算同时进行,并且采用不同的运算顺序来提高性能:

1

2

3

4

5

6

7

8

9

10

for ( int i=0; i length; i+=2) {

sum += array[i];

sum += array[i+1];

}

---------------------------------

for ( int i=0; i length; i+=2) {

sum1 += array[i];

sum2 += array[i+1];

}

sum = sum1 + sum2;

在这两个代码段中,区别在于在同一个循环中,后者使用了两个变量。在指令级别上,在同时执行循环里指令时,由于存在数据依赖,必须在第一次求和结束后,拿到这个sum值,才能进行第二次求和。因此,两个变量可以使得多个运算单元同时执行求值。

采用不同的求值顺序在长表达式中也非常重要,这个话题需要深入到处理器指令的关键路径上。不了解的话可以采用实验的方式求解同一条表达式:

a = (b * c ) * (d * e) * f

a = ((b * c) * d) * e * f

在处理条件分支时,采用函数式风格来帮助编译器使用条件数据移动。

在处理器碰到分支结构时,在判断完成前,需要执行下一条指令,这时候,处理器会随机执行下一个指令,有可能会判断出错。如果判断出错,处理器需要回到之前的状态,然后执行正确的指令。这里涉及到处理器中的Retirement Unit,它会保存执行分支指令的状态,并且使用(register name, value)的方式传递假如判断正确的事先已经完成的指令所产生的寄存器的状态变化结果。如果判断出错,只要清空之前保存的()。

因此,在碰到执行错误分支时,程序性能会下降,程序员需要帮助编译器来使用分支数据选择而不是分支指令选择。

考虑以下函数,排序两个数组,使得对于每个i, b[i] = a[i]:

这两个函数区别在于,第一个采用分支控制选择,第二个采用分支数据选择。分支控制选择在判断出错后会影响运行性能,而分支数据选择可以让编译器采取cmov式的汇编指令来避免判断出错。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

void minmax1( int a[], intb[], int n) {

int i;

for (i = 0; i n; i++) {

if (a[i] b[i]) {

int t = a[i];

a[i] = b[i];

b[i] = t;

}

}

}

void minmax2( int a[], intb[], int n) {

int i;

for (i = 0; i n; i++) {

int min = a[i] b[i] ? a[i] : b[i];

int max = a[i] b[i] ? b[i] : a[i];

a[i] = min;

b[i] = max;

}

}

在整个优化中,你会发现函数式风格的重要性,因为在现代处理器的设计中,采用了out of order的指令执行,只要保证最终行为的一致,处理器执行指令时会采用同时流水线执行多条指令,来加快运行速度。这时候,数据状态的变化会严重影响处理器的并行化,在命令式编程语言中同样需要函数式的编程。

在处理器设计上,会发现无状态是最自然的方式,改变数据状态反而显得有点背离。函数式语言不改变数据状态这个特点会越来越突出,因为在处理器级别上,无状态数据是最适合的方式,使得处理器节省不必要的执行,在上面的分支结构选择和数据依赖上已经很好的体现了。

最后,最重要的优化原则是:在瓶颈上优化!

经作者同意,转载自博客

喜欢就请分享转发!

ssdfans群介绍

技术讨论群 覆盖2000多位中国和世界华人圈SSD以及相关存储行业的技术精英 固件、软件、测试群 SSD固件、软件和测试技术讨论 异构计算群 讨论人工智能和GPU、FPGA、CPU异构计算 ASIC-FPGA群 芯片和FPGA硬件技术讨论群 闪存器件群 NAND、3D XPoint等固态存储介质技术讨论 企业级 企业级SSD技术与应用 销售群 SSD,NAND Flash交易 工作求职群 SSD行业换工作,发招聘,要关注各大公司招聘信息,赶快来   想加入这些群,请加nanoarch为好友,介绍你的昵称-行业-职务,注明群名,拉你进群。

相关推荐