C代码的简单优化方法(机器:intel酷睿i7,摘自csapp)

本文探讨了代码优化的各种方法,包括消除内存别名使用、函数调用优化、连续函数调用消除、不必要的内存引用减少。介绍了循环展开降低循环开销,通过增加每次迭代计算的元素数量来减少循环次数,以及如何通过多个累积变量和重新结合技术提高指令的并行性。同时,文章强调了在条件语句中使用功能性风格以避免分支预测错误的开销,并提出通过重新排列循环来提高空间局部性,从而减少缓存未命中次数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【概念介绍】
1、CPE :每元素周期数,帮助我们理解循环性能。
执行一个循环用的时钟周期数 = 总时钟周期 / 元素数
我们更愿意用每个元素(循环体里面的重复执行的部分)的周期数来衡量速度,而不是每次循环的周期数。因为像循环展开,我们用更少的循环来完成计算
运行时间:368+9.0n =准备循环以及完成过程的开销是368+每个元素9个周期的线性因子

解释什么叫元素周期:
法一:
for(i = 0 ; i < n; i++){
	b[i] = a[i];
}
循环展开:
for(i = 0; i < n-1; i += 2){
	b[i] = a[i];
	b[i+1] = a[i + 1];
}
for(i; i < n ;i++){
	b[i] = a[i];
}
此时的元素数是 n 

【方法】

一、编译的不优化

1、不优化内存别名使用

这种两个指针可能指向同一内存位置的情况叫 内存别名使用
示例1:
voi twiddle1(long *x,long *y)
{
	*x += *y;
	*x += *y;
}

改进:
voi twiddle2(long *x,long *y)
{
	*x += 2 * (*y);
}
这种合并在两个指针指向同一地址会出错

2、不优化函数调用

long f();

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

改进:
long fun2(){
	return 4*f();
}
如果函数有副作用,比如
f(){
	全局变量 count ++;
    或者其他影响下次调用的代码
}
如果编译器像程序员这样优化就出错了,所以编译器拒绝函数调用优化;

解决办法: 内联
long fun2(){
	t += count;
	t += count;
	t += count;
	return t;
}

二、消除连续的函数调用
1、代码移动
把在循环里要执行多次,但是结果不会改变的计算,移到循环外面

for(i = 0; i < strlen(s), i++){
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
}

改进
int len = strlen(s)for(i = 0; i < len , i++){
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
}
在数据很大的时候这个效率差距差了近千倍
未优化的 抽象的 goto语言的版本
	i = 0;
loop:
	if(i >= strlen(s)) //这要执行多少次???
		goto end;
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
	i ++;
	if(i < strlen(s))
		goto loop
end:

三、消除不必要的内存引用
过多的变量会存在栈中,含(指针引用)的也会存在栈中

typedef struct{
	long len;
	int *data;
}*vec_ptr;

int* get_start(vec_ptr v){
	return v->data;
}
整数求和 写法1:
传入参数 *sum{
int *data =  get_start (v);
for(i = 0; i < len ;i++){
	*sum = *sum + data[i];
}}
整数求和 写法2int acc;
for(i = 0; i < len ;i++){
	acc = acc + data[i];
}
*dest = acc;

此处的内存引用是指针的引用。
超标的寄存器变量和指针都是存在特殊的内存———栈中

四、概念补充
【1】整数加、乘和浮点数加和乘是完全流水化的
除法占有3~30、3-15浮点 个时钟周期,而且没有完全流水化····
【2】延迟界限L:完成合并运算的函数所需要的最小的CPE值
计算n个单元的时钟周期:L*n + K
请添加图片描述

五、低级优化

1. 展开循环降低开销 k * 1
【 原理】:增加每次迭代计算的元素的数量,减少循环迭代次数。
首先,减少了循环索引计算和条件分支
其次,减少整个计算中关键路径上的操作数量
【效果】对加法效果明显,对其他运算没有太大影响

//k*1循环展开,以5*1循环展开为例
for(i = 0; i < len-4; i += 5){
	a = (a + data[i])+data[i+1];
	a = (a + data[i+2])+data[i+3];
	a = a + data[i+4];
}
for(i; i < len ;i++){
	a = a + data[i];
}

2. 通过使用例如多个累计变量和重新结合等技术,找到方法提高指令的并行性
【硬件特性】
1)容量C:4个加法器件,2个浮点乘器件,其余一个
2)发射I:除了 除法 都实现了流水化
3)延迟L:整数+1,整数乘3,浮点+3,浮点5,除都很多3-30、3-15
执行加法和乘法是完全流水化的并且有多个功能单元一起
4)循环寄存器之间的操作链,决定了限制性能的数据相关
请添加图片描述
请添加图片描述
补充说明:在流水线中,一个浮点乘法要5个时钟周期
在运算里面本来是三个乘法,如果关键路径用了三个乘法就是 5
3/3;如果用了两个乘法就是5*2/3这样算那个最少的CPE

办法1】多个累积变量 k * k
1)多个累积变量:将一个运算拆成多个运算,并在最后合并结果来提高性能
2)要求 (k* k循环展开,k 最小是多少)
只有保持该循环操作的所有的功能器件都是满的,才会达到最大吞吐量
要求循环展开因子 k >= C * L
C是容量,L是延迟;
例如:浮点乘法 2*5 ,k>=10;浮点加k>=3
对整数乘法、浮点加法、浮点乘法性能约提升2倍

2*2 展开
计算前n个的乘积
for(i = 0 ; i < len-1 ;i+=2){
	acc0 = acc0 * data[i];//奇数积 循环acc0
	acc1 = acc1 * data[i+1];//偶数积 循环acc1
}
for(i ; i < len ;i++)
	acc0 = acc0 * a[i];//落单积
*dest = acc0 * acc1;//总积
内循环包括两个乘法操作,被翻译成读写不同的乘法功能单元
风险:
整数的加法和乘法是可交换、可结合的
但是浮点数的加法和乘法是不可结合的

办法2】重新结合变换 k * 1a
1)重新结合变化:
针对于乘法的k1循环展开,改变括号位置,除整数加法,都要比k1循环展开效果好
2) 优势
一个循环每次迭代两个乘法
优势就在于第一个乘法,不需要等待前一次迭代的累积值就可以执行,所以最小的CPE少了2倍
(data[i] * data[i+1]),乘法的两个数,直接从内存加载就可以了,不需要的等待乘数a

2*1
for(i = 0; i < len-1; i += 2){
	a = (a * data[i]) * data[i+1];//a是放在循环寄存器里面
}

2*1a
for(i = 0; i < len-1; i += 2){
	a = a * (data[i] * data[i+1]);//a是放在循环寄存器里面
}

注意:
如果并行度太高,并行度P超过了可用寄存器数量,编译器会将某些值放入内存中。如k=20不如k=10的效率好。
x86-64上有16个寄存器,可以使用16个浮点数寄存器,所以循环变量不要超过可用寄存器数量

3. 在条件语句中,用功能性风格使编译器采用条件数据传送,避免分支预测错误的开销
【方法】
改变编码风格,让汇编代码执行条件传送
就是先两种结果都计算,用的时候再比较条件,选择哪个是对的再输出

条件控制
for(i = 0; i < len ;i ++){
	if(a[i]>b[i]){
	    t = a[i];
		a[i] = b[i];
		b[i] = t;
	}
}

条件传送
for(i = 0; i < len ;i ++){
	min = a[i] < b[i] ? a[i]:b[i];
	max = a[i] > b[i] ? a[i]:b[i];
	a[i] = min;
	b[i] = max;
}

六、重新排列循环以提高空间局部性
1)数组按行存储,最好ijk顺寻访问,步长为1
2)一旦读入了一条数据,尽可能的多地使用它

计算C矩阵= A矩阵*B矩阵
巧妙利用寄存器使cache的未命中次数从1.25降低到0.5

1.25
for(i = 0; i < n; i++)
{
	for(j = 0; j < n; j++ )
	{
		//对C数组进行循环
		sum = 0;
		for(k = 0; k < n; k++){
			sum += A[i][k]*B[k][j];
		}
		c[i][j] += sum;
	}
}

未命中总次数0.5
for(k = 0; k < n; k++){
	for(i = 0; i < n; i++)
	{
		r = A[i][k];
		for(j = 0; j < n; j++ )
		{
			c[i][j] += r * B[k][j];
		}
	}
}
1、选择合适的算法和数据结构 2、使用尽量小的数据类型 3、减少运算的强度 (1)查表 (2)求余运算 (3)平方运算 (4)用移位实现乘除法运算 (5)避免不必要的整数除法 (6)使用增量和减量操作符 (7)使用复合赋值表达式 (8)提取公共的子表达式 4、结构体成员的布局 (1)按数据类型的长度排序 (2)把结构体填充成最长类型长度的整倍数 (3)按数据类型的长度排序本地变量 (4)把频繁使用的指针型参数拷贝到本地变量 5、循环优化 (1)充分分解小的循环 (2)提取公共部分 (3)延时函数 (4)while循环和do…while循环 (5)循环展开 (6)循环嵌套 (7)Switch语句中根据发生频率来进行case排序 (8)将大的switch语句转为嵌套switch语句 (9)循环转置 (10)公用代码块 (11)提升循环的性能 (12)选择好的无限循环 6、提高CPU的并行性 (1)使用并行代码 (2)避免没有必要的读写依赖 7、循环不变计算 8、函数 (1)Inline函数 (2)不定义不使用的返回值 (3)减少函数调用参数 (4)所有函数都应该有原型定义 (5)尽可能使用常量(const) (6)把本地函数声明为静态的(static) 9、采用递归 10、变量 (1)register变量 (2)同时声明多个变量优于单独声明变量 (3)短变量名优于长变量名,应尽量使变量名短一点 (4)在循环开始前声明变量 11、使用嵌套的if结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值