优化程序性能

2021/12/29 22:08:15

本文主要是介绍优化程序性能,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

注:以下所有内容均来自开源学习组织DataWhale

优化程序性能

编写高效程序需要满足以下条件:

  • 选择合适的算法和数据结构
  • 理解编译器的能力和局限性
  • 探索并行化

编译器优化程序的局限性

例C代码如下:

void add1(long *xp, long *yp) {
    *xp += *yp;
    *xp += *yp;
}

void add2(long *xp, long *yp) {
    *xp += 2 * *yp;
}

两者功能相同,都是将指针yp指向的数两次加到指针xp指向的数。但add2的执行效率更高,原因是add1需要进行6次内存引用(包括两次读xp指向的位置,两次读yp指向的位置,两次写xp指向的位置),而add2只需要执行3次内存引用(一次读xp指向的位置,一次读yp指向的位置,一次写xp指向的位置)。

对于如下C代码:

long f();

long func1() {
	return f() + f() + f() + f();
}

long func2() {
	return 4 * f();
}

func1中f被调用4次,而func2中f被调用1次。

加入f内容如下:

long counter = 0;

long f() {
	return counter++;
}

则func1会返回6,而func2会返回0。

因此编译器无法确定利用上述方式对代码进行优化。

每元素周期数(Cycles Per Element,CPE)

对于一个程序,如果我们记录该程序的数据规模以及对应的运行所需的时钟周期, 并通过最小二乘法来拟合这些点,我们将得到形如 y = a + bx 的表达式,其中 y 是 时钟周期,x 是数据规模。当数据规模较大时,运行时间就主要由线性因子 b 来决 定。这时候,我们将 b 作为度量程序性能的标准,称为每元素的周期数(Cycles Per Element, CPE)。

CPE越小,程序性能越高

代码移动

减少函数调用以提升性能

对于如下C代码

void combine1(vec_ptr v, data_t *dest) {
    long i;
    *dest = IDENT;
    
    for (i = 0; i < vec_length(v); i++) {
        data_t val;
        get_vec_element(v, i, &val);
        *dest = *dest OP val;
    }
}

其对应整型加法运算和乘法运算的CPE值分别为10.12和10.12,对应浮点型加法运算和乘法运算的CPE值分别为10.17和11.14。

上面代码中每次循环都会调用函数vec_length(v)一次,可以考虑将其移除循环,如下所示:

void combine2(vec_ptr v, data_t *dest) {
    long i;
    *dest = IDENT;
    
    long length = vec_length(v) // 将其赋给一个变量
    for (i = 0; i < length; i++) {
        data_t val;
        get_vec_element(v, i, &val);
        *dest = *dest OP val;
    }
}

经过上述优化后,其对应整型加法运算和乘法运算的CPE值分别为7.02和10.12,对应浮点型加法运算和乘法运算的CPE值分别为9.02和11.03。可以发现程序性能有了一些提升。

继续将循环里的get_vec_element函数拿出循环,如下:

data_t *get_vec_start(vec_ptr v) {
	return v -> data;
}

void combine3(vec_ptr v, data_t *dest) {
    long i;
    long length = vec_length(v);
    data_t *data = get_vec_start(v);

    *dest = IDENT;
    for (i = 0; i < length; i++)
    	*dest = *dest OP data[i];
}

经过上述操作,combine3的CPE值如下:

在这里插入图片描述

可以发现,将get_vec_element拿出循环后,CPE值并没有降低,程序性能没有提升。这说明循环中有其他的操作成为了优化性能的瓶颈。

消除不必要的内存引用

combine3函数对应汇编代码如下:

.L17:
    vmovsd (%rbx), %xmm0
    vmulsd (%rdx), %xmm0, %xmm0
    vmovsd %xmm0, (%rbx)
    addq $8, %rdx
    cmpq %rax, %rdx
    jne .L17

每次迭代时,累积变量的数值都要从内存中再读出写入到内存,可以通过如下代码消除:

void combine4(vec_ptr v, data_t *dest) {
    long i;
    long 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;
}

经过上述操作,combine4的CPE值如下:

在这里插入图片描述

可以发现,消除不必要的内存引用后,combine4的CPE值明显降低,说明程序性能提升显著。

现代处理器的优化

  • 近期的 Intel 处理器是**超标量(superscalar)**的,意思是它可以在每个时钟周期执行 多个操作。此外还是 乱序的(out-of-order),意思是指令执行的顺序不一定与机器 级程序中的顺序一致。

  • 这样的设计使得处理器能够达到更高的并行度。例如,在执行分支结构的程序时, 处理器会采用**分支预测(branch prediction)**技术,来预测是否需要选择分支,同 时还预测分支的目标地址。

  • 此外还有一种**投机执行(speculative execution)**技术,意思是处理器会在分支之 前就执行分支之后的操作。如果预测错误,那么处理器就会将状态重置到分支点的 状态。

循环展开

循环展开即通过增加每次迭代计算的元素数量,来减少循环的迭代次数,以提升程序性能。

例如有如下C代码:

void psum1(float a[], float p[], long n) {
    long i;
    p[0] = a[0];
    for (i = 1; i < n; i++)
    	p[i] = p[i-1] + a[i];
}

循环展开形式:

void psum2(float a[], float p[], long n) {
    long i;
    p[0] = a[0];
    for (i = 1; i < n - 1; i += 2) {
        float mid_val = p[i-1] + a[i];
        p[i] = mid_val;
        p[i+1] = mid_val + a[i+1];
}
	if (i < n)
		p[i] = p[i-1] + a[i];
}

对于前面的函数combine4,也可以对其进行循环展开,形式如下:

void combine5(vec_ptr v, data_t *dest) {
    long i;
    long length = vec_length(v);
    long limit = length - 1; // 防止数组越界
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;

    for(i = 0; i < limit; i +=2){
        acc = (acc OP data[i]) OP data[i+1]
    }
    for (; i < length; i++){
        acc = acc OP data[i];
    }
    *dest = acc;
}

循环展开后CPE值如下:

在这里插入图片描述

可以发现,性能有一些提升。

提升性能的另一种方式是调整combine5中括号位置,如下:

void combine7(vec_ptr v, data_t *dest) {
    long i;
    long length = vec_length(v);
    long limit = length - 1; // 防止数组越界
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;

    for(i = 0; i < limit; i +=2){
        acc = acc OP (data[i] OP data[i+1])
    }
    for (; i < length; i++){
        acc = acc OP data[i];
    }
    *dest = acc;
}

combine7的CPE结果如下:

在这里插入图片描述

可以发现程序性能有明显提升。这是由于每次迭代的第一个乘法操作都不需要等待前一次迭代的累计值就可以执行,因此程序性能更好。



这篇关于优化程序性能的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程