CMU 15-213学习 第五章程序性能优化

467 阅读1分钟

程序性能优化

这里描述了两个会阻止编译器优化的场景。

例子1. 将C风格的string转换成小写

void lower(char *s)
{
    size_t i;
    for (i = 0; i < strlen(s); i++)
    {
        if (s[i] > 'A' && s[i] <= 'Z')
            s[i] -= ('A' - 'a');
	}
}

这段代码的问题在于strlen(s)被重复运算

strlen会遍历字符串,直到找到空字符为止,函数为线性复杂度,lower算法复杂度为O(n^2)

编译器无法优化函数调用的代码

  1. 不确定函数中是否会有副作用。
  2. 函数不一定会返回相同的值。

在例子1中,循环体会修改s的值,编译器无法确保strlen(s)不变

改为如下:

void lower(char *s)
{
    size_t i;
    size_t len = strlen(s);
    for (i = 0; i < len; i++)
    {
        if (s[i] > 'A' && s[i] <= 'Z')
            s[i] -= ('A' - 'a');
    }
}

函数复杂度被修改为O(n)

例子2. 计算矩阵每行的和

/ * Sum rows is of n X n matrix a and store in vectorb */
void sum_rows1(double *a, double *b, long n)
{
    long i, j;
    for (i = 0; i < n; i++)
    {
        b[i] = 0;
        for (j = 0; j < n; j++)
        {
            b[i] += a[i * n + j];
        }
	}
}

这段代码表面上看上去没问题(当然,可以移动i * n到外部),我们看一下内循环汇编后的代码

# sum_rows1 inner loop
.L4:
		movsd	(%rsi, %rax, 8) %xmm0	# FP load
		addsd	(%rdi), %xmm0			# FP add
		movsd 	%xmm0, (%rsi, %rax, 8)	# FP store
		addq	$8, %rdi
		cmpq	%rxc, %rdi
		jne		.L4

每次循环都需要从内存中读取b[i],做完加法后写回内存。这里每一次取b[i]的值,与上一次写回b[i]的值是一样的。数据在内存和寄存器之间来回传送。

编译器假设存在内存别名

在例子中,b与a的地址有重合的部分,对b[i]的写入可能会影响a[i]的值,从而影响结果求和,所以需要编译器不能优化这段代码,每一次都需要对内存进行读写。

这里可以引入一个局部变量存储每行累加的值,在换行的时候将值赋给b[i]

/ * Sum rows is of n X n matrix a and store in vectorb */
void sum_rows1(double *a, double *b, long n)
{
    long i, j;
    for (i = 0; i < n; i++)
    {
        double val = 0;
        for (j = 0; j < n; j++)
        {
            val += a[i * n + j];
        }
        b[i] = val;
	}
}

内循环对应的汇编代码:

# sum_rows2 inner loop
.L10:
		addsd	(%rdi), %xmm0	# FP load + add
		addq	$8, %rdi
		cmpq	%rax, %rdi
		jne		.L10

进行测试后发现性能有略微的提升,并没有例子1这么明显。