HLS_循环优化

前提

  在进行循环优化的阐述之前,需要了解一些基础概念,数据依赖,那么什么是数据依赖呢?大致有如下几种:
  顺序依赖,又称真依赖,S1定义了一个值,随后S2中使用了这个值

S1:X=A+B
S2:Y=X+A

  如上所示:S2语句执行得到Y值,必须要在S1语句执行得到X值后才能进行,可能有的人会想他不会自动把S2语句中的X值替换成A+B吗,他哪有你这个大聪明聪明呀,实际的执行过程是将A+B的结果存起来,等到S2语句的时候再取出来。
  反依赖,S1中使用了这个值,而随后S2定义了这个值

S1:A=X+B
S2:X=C+A

  输出依赖,两个语句都定义了一个值

S1:X=A+B
S2:X=C+D

  输入依赖,两个语句都使用了一个值

S1:X=A+B
S2:Y=A+D

  以上四种数据依赖,前3种都约束了语句的执行顺序,也即不能打乱S1和S2前后执行顺序。
  局部性:又可细分为时间局部性和空间局部性,时间局部性,即很快还会访问这个内存,可以保持要访问的内容保持在cache中,空间局部性,即下一次访问的地址与上次访问的距离很近,就是让要用的代码和数据尽可能紧凑,尽可能小。

循环展开

  循环展开,英文名叫Loop Unrolling,通过牺牲程序的尺寸来加速程序的执行速度的优化方法,通过将循环体内的代码复制多次的操作,进而减少循环分支指令执行的次数,增大处理器指令调度的空间,获得更多的指令级并行。与之相反有个概念叫做循环压紧,可做类比。
  其优点在于:
  1、减少循环分支指令执行的次数;
  2、获得了更多的指令级并行;
  3、增加了寄存器的重用。

for(i=0;i<N;i++)
{
	for(j=0;j<N;j++)
	{
		A[i][j] = A[i][j] + B[i][j] * C[i][j]
	}
}

对j层进行循环展开后如下:

for(i=0;i<N;i++)
{
	for(j=0;j<N;j+=4)
	{
		A[i][j] = A[i][j] + B[i][j] * C[i][j];
		A[i][j+1] = A[i][j+1] + B[i][j+1] * C[i][j+1];
		A[i][j+2] = A[i][j+2] + B[i][j+2] * C[i][j+2];
		A[i][j+3] = A[i][j+3] + B[i][j+3] * C[i][j+3];
	}
}

上面这种展开方式是不会违反循环展开的合法性的,再来看看下面这种:

for(i=1;i<N;i++)
{
	for(j=1;j<N;j++)
	{
		A[i+1][j-1] = A[i][j] + 3;
	}
}

//对i层进行循环展开后
for(i=1;i<N;i+=2)
{
	for(j=1;j<N;j++)
	{
		A[i+1][j-1] = A[i][j] + 3;
		A[i+2][j-1] = A[i+1][j] + 3;
	}
}

举例说明,在展开前,A[2][1]这个地址首先写入A[1][2] +3的值,在随后该地址的值又被取出用于计算A[3][0],所以对于A[2][1]而言,属于先写后读的情况,展开以后,当i=j=1时,A[3][0]=A[2][1]+3,此时是A[2][1]是读取状态,当i=1,j=2时,A[2][1]=A[1][2]+3,此时A[2][1]是写入状态,那么对于A[2][1]而言属于是先读后写的情况,如此一来,便改变了原语义,因此这样的循环展开是不合法的。

循环合并

  循环合并,英文名叫Loop Fusion,是将具有相同迭代空间的两个循环合成一个循环的过程,属于语句层次的循环变换。
  其优点在于:
  1、减小循环的迭代开销
  2、增强数据重用,寄存器重用
  3、减小并行化的启动开销,消除合并前多个循环间的线程同步开销
  4、增加循环优化的范围
  注意:这里对于数据重用的理解:就拿下面的例子来说明,A数组首先被全部定义完,随后在下一个循环块中被调用,假设N值很大,当写完A数组的最后一个数据时,A[0]中的数据已不处于缓存中,缓存可类比于cache理解,那么在下一个循环块中使用时需要被重新加载到缓存中,合并之后,A[0]中的值能够被及时使用,cache miss减少一半,此时数据局部性最好。

//合并前
for(j=0;j<N;j++)
{
	A[j] =  B[j] + C[j];
}
for(j=0;j<N;j++)
{
	D[j] =  B[j] - C[j];
}
//合并后
for(j=0;j<N;j++)
{
	A[j] =  B[j] + C[j];
	D[j] =  B[j] - C[j];
}

  同样地,循环合并不能改变原有的依赖关系,也不能产生新的依赖,观察下面的错误实例:

//合并前
for(i=1;i<N;i++)
{
	A[i] = B[i] + C;
}
for(i=1;i<N;i++)
{
	D[i] = A[i+1] +E;
}
//合并后
for(i=1;i<N;i++)
{
	A[i] = B[i] + C;  //S1
	D[i] = A[i+1] +E; //S2
}

  合并前他们之间为真依赖关系,合并后,同处于一个循环块中,例如A[2]在前一刻被S2语句使用,而在后一时刻在S1语句中被定义,这属于依赖关系中的反依赖,如此一来改变了原有的依赖关系,因此这样的循环合并是不合法的,不能被合并。

循环分布

  循环分布,英文名叫Loop Distribute,将一个循环分解为多个循环,且每个循环都有与原循环相同的迭代空间,但只包含原循环语句子集,常用于分解出可向量化或者可并行化的循环,进而将可向量化部分的代码转为向量执行。
  其优点在于:
1、将一个串行循环转变为多个并行循环
2、实现循环的部分并行化
3、增加循环优化的范围
  其缺点在于:
1、减小并行性粒度
2、增加额外的通信和同步开销

//分布前
for(int i=0;i<N;i++)
{
	A[i] = i;
	B[i] = 2 + B[i];
	C[i] = 3 + C[i-1];
}
//分布后
for(int i=0;i<N;i++)
{
	A[i] = i;
	B[i] = 2 + B[i];
}
for(int i=0;i<N;i++)
{
	C[i] = 3 + C[i-1];
}

由于C数组存在依赖关系,只能串行执行,因此将其分离出来,对剩余部分即可进行并行化或者向量化操作。

循环交换

  当一个循环块内包含一个以上的循环块,且循环块之间不包含其他语句,则称这个循环为紧嵌套循环,交换紧嵌套循环中两个循环块的嵌套顺序是提高程序性能最有效的变换之一。实际上,循环交换是一个重排序变换,仅改变了参数化迭代的执行顺序,但是并没有删除任何语句或产生新的语句,所以循环交换的合法性的合法性需要通过循环的依赖关系进行判定。
  其优点在于:
  1、增强数据局部性
  2、增强向量化和并行化的识别

//交换前
for(i=0;i<N;i++)
 	for(j=0;j<N;j++)
 		for(k=0;k<N;k++)
 			A[i][j] = A[i][j] + B[i][k] * C[k][j];
//交换后
for(i=0;i<N;i++)
	for(k=0;k<N;k++)
 		for(j=0;j<N;j++)
 			A[i][j] = A[i][j] + B[i][k] * C[k][j];

  最内层循环决定了数组的哪一维被优先访问,A数组处于读写状态,B\C数组处于读状态,相邻两次循环中,A[i][j]保持不便,B[i][k]和B[i][k+1]相邻,不会发生cache miss的情形,但是C[k][j]和C[k+1][j]相隔一行,违反了cache局部性原则,交换后,地址相邻,连续两次访问命中cache的概率非常高。
  再来看下面的示例:

//交换前
for(i=0;i<N;i++)
 	for(j=0;j<N;j++)
 			A[i][j+1] = A[i][j] + 2;
//交换后
for(j=0;j<N;j++)
 	for(i=0;i<N;i++)
 			A[i][j+1] = A[i][j] + 2;

  交换前存在真依赖关系,例如A[0][1]在前一刻被定义,后一刻被使用,而交换后,依旧存在真依赖关系,只不过该依赖关系被带到了外层循环,在内层循环中则不存在依赖关系,那么内层就可以进行并行化,需要注意的是在循环交换时不能改变其依赖关系,例如下面的错误示例:

//交换前
for(j=0;j<N;j++)
 	for(i=0;i<N;i++)
 			A[i][j+1] = A[i+1][j] + 2;
//交换后
for(i=0;i<N;i++)
 	for(j=0;j<N;j++)
 			A[i][j+1] = A[i+1][j] + 2;

  循环交换前,A[2][2]=A[3][1]+2先被定义,而后A[1][3]=A[2][2]+2被使用,属于真依赖关系,循环交换后,A[2][2]先被使用,后被定义,属于反依赖关系,此交换不合法。

循环不变量外提

  在循环迭代空间内不发生变化的变量,由于循环不变量的值在循环的迭代空间内不发生变化,因此可将其外提到循环外仅计算一次,避免其在循环体内重复计算。

for(int i = 1;i<N;i++)
	for(int j=1;j<M;j++)
		U[i]=U[i]+W[i]*W[i]*D[j];

for(int i = 1;i<N;i++)
	T1=W[i]*W[i];
	for(int j=1;j<M;j++)
		U[i]=U[i]+T*D[j];

注意变化不能影响源程序的语义。其优点在于削弱计算强度。

循环分段

  将单层循环变换为两层嵌套循环,内层循环遍历的是迭代次数为strip的连续区域(或叫做条带循环),外层循环的步进单位为strip,这个strip叫做分段因子。

for(int i = 0;i<N;i++)
	A[i]=B[i]+C[i];

for(i=0;i<N;i+=k)
	for(j=i;j<i+k-1;j++)
		A[j]=B[j]+C[j];

其优点在于充分利用多个处理器资源,将可用的并行性转换成更适合硬件的形式。

循环分块

  循环分块是指通过增加循环嵌套的维度来提升数据局部性的循环变换技术,是对多重循环的迭代空间进行重新划分的过程,可以看作是对循环分段和循环交换的集合。

for(i=0;i<N;i++)
	for(j=0;j<N;j++)
		B[i]=B[i]+A[i][j];
//先进行循环分段		
S=4;		
for(i=0;i<N;i++)
	for(j=0;j<N;j+=S)
		for(k=j;k<=MIN(j+S-1,N);k++)
			B[i]=B[i]+A[i][k];
//再进行循环交换
S=4;
for(j=0;j<N;j+=S)		
	for(i=0;i<N;i++)
		for(k=j;k<=MIN(j+S-1,N);k++)
			B[i]=B[i]+A[i][k];

如此一来,最内层循环即可进行并行化操作。

循环分裂

  是将循环的迭代次数拆成两段或者多段,拆分后的循环不存在主体循环之说,也就是拆分成迭代次数都比较多的两个或者多个循环。

for(i=1;i<N;i++)
	A[i]=A[i]+A[M];

for(i=1;i<M;i++)
	A[i]=A[i]+A[M];
for(i=M;i<N;i++)
	A[i]=A[i]+A[M];

再来看下面的第二个例子展示循环分裂的有利性,数组coff的计算结果用到了数组a和数组b,而数组diff的计算结果用到了数组c和数组d,且其计算的为数组下标M到N的值,可以通过循环分裂将数组coff和数组diff的计算分开,得到两个循环,优化后的循环内引用数组的数量减少,这样每个循环的数据局部性都得到了提升。

for(i=0;i<N;i++)
	temp = a[i] - b[i];
	coff[i] = (a[i]+b[i])*temp;
	diff[i+M] = (c[i+M]+d[i+M])/phi;
	
for(i=0;i<N;i++)
	temp = a[i] - b[i];
	coff[i] = (a[i]+b[i])*temp;

for(i=M;i<N;i++)
	diff[i] = (c[i]+d[i])/phi;

参考文章:
1、数据依赖和控制依赖 Data Dependence and Contol Dependence
2、循环分块
3、循环优化之循环合并
4、循环交换基本概念
5、循环交换与局部性

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值