C程序性能优化方法(三)

2.8 循环展开

  • 循环展开是一种程序变换,通过增加每次迭代计算的元素数量,减少循环迭代次数。循环展开将从两个方面改进程序的性能。首先,他减少了不直接有助于程序结构的操作数量,例如循环索引计算和条件分支。第二,他提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。
  • 一般来说,向量的长度不一定是2的倍数。想要使我们的代码对任意向量长度都能正确的工作,可以从两个方面解释这个需求。首先,要确保第一次循环不会超出数据的界限。对于长度为n的向量,我们将循环界限设置为n-1。然后,保证只有当循环索引i满足i<n-1时才会执行这个循环,因此最大数据索引i+1满足 i+1 < (n-1) + 1 = n。
  • 根据以上思想,将其归纳为对一个循环按任意因子k进行展开,由此产生k * 1循环展开。为此,其上限将设置为n-k+1,在循环内对元素i到i+k-1应用合并运算。每次迭代,循环索引i加k,那么最大循环索引i+k-1会小于n。要使用第二个循环,以每次处理一个元素的方式处理向量的最后几个元素。
1	void combine5(vec_ptr v, data_t *dest)
2	{
3		long i;
4		long length = vec_length(v);
5       long limit = length - 1;
6	    data_t *data = get_vec_start(v);        
7	    data_t acc = IDENT;	
8		
9        /* Combine 2 element at a time */
10		for(i=0; i<length; i+=2)
11		{
12  		acc = (acc OP data[i]) OP data[i+1];
13  	}
14
15       /* Finish any remaining elements */
16      for(; i<length; i++)
17      {   
18          acc = acc OP data[i];
19      }
20      *dest = acc;
21 	}
  • 当测量展开次数 k = 2和k = 3 的展开代码的性能是,得到下面的测试结果:
    这里写图片描述
  • 备注:让编译器展开循环
    编译器可以很容易的执行循环展开。只要优化级别设置得足够高,许多编译器都能例行公事地做到这一点。用优化等级3或更高等级调用GCC,它就会执行循环展开。

2.9 提高并行性

  • 在此,程序的性能是受到运算单元的延迟限制的,执行加法和乘法的功能单元完全流水线化的,这意味着它们可以每个时钟周期开始一个新的操作,并且有些操作可以被多个功能单元执行。硬件具有以更高速率执行乘法和加法的潜力,但是代码不能利用这种能力,即使是使用循环展开也不能,这是因为我们将累计值放在一个单独的变量acc中。在前面的计算完成之前,都不能计算acc的新值。虽然计算acc新值的功能单元能够每个时钟周期开始一个新的操作,但是它只会每L个周期开始一条新操作,这里L是合并操作的延迟。现在我们要考擦打破这种顺序相关,得到比延迟界限更好性能的方法。

2.9.1 多个累积变量

  • 对于一个可结合和可交换的合并运算来说,比如整数加法或乘法,我们可以通过将一组合并运算分割成两个或更过的部分,并在最后合并结果来提高性能。
1	void combine6(vec_ptr v, data_t *dest)
2	{
3		long i;
4		long length = vec_length(v);
5       long limit = length - 1;
6	    data_t *data = get_vec_start(v);        
7	    data_t acc0 = IDENT;
8	    data_t acc1 = IDENT;	
9   	
10      /* Combine 2 element at a time */
11  	for(i=0; i<length; i+=2)
12  	{
13    		acc0 = acc0 OP data[i];
14          acc1 = acc1 OP data[i+1];
15    	}
16  
17      /* Finish any remaining elements */
18      for(; i<length; i++)
19      {   
20          acc0 = acc0 OP data[i];
21      }
22      *dest = acc0 OP acc1;
23 	}
  • Combine6既使用了两次循环展开,以使每次迭代合并更过的元素,也使用两路并行,将索引值为偶数的元素累积在变量acc0中,而索引值为计数的元素累积在变量acc1中。于是,我们将其称为“2 x 2”循环展开。对于循环长度不为2的倍数时,这个循环要累积所有剩下的数组元素。然后,我们在对acc0和acc1应用合并运算,计算最终的结果。
    比较只做循环展开和既做循环展开同时也使用两路并行的方法,我们得到下面的性能:
    这里写图片描述

2.9.2 重新结合变换

1	void combine7(vec_ptr v, data_t *dest)
2	{
3		long i;
4		long length = vec_length(v);
5       long limit = length - 1;
6	    data_t *data = get_vec_start(v);        
7	    data_t acc = IDENT;	
8		
9        /* Combine 2 element at a time */
10		for(i=0; i<length; i+=2)
11		{
12  		acc = acc OP (data[i] OP data[i+1]);
13  	}
14
15       /* Finish any remaining elements */
16      for(; i<length; i++)
17      {   
18          acc = acc OP data[i];
19      }
20      *dest = acc;
21 	}
  • 上述函数与combine5的唯一差异点在于内循环中元素的合并方式。在combine5中,合并是下面这条语句来实现的:
    12 acc = (acc OP data[i]) OP data[i+1];
    而在combine7中,合并是以这条语句来实现的:
    12 acc = acc OP (data[i] OP data[i+1]);
  • 差别在于两个括号是如何放置的。我们称之为重新结合变换,因为括号改变了向量元素与累计值acc的合并顺序,产生了我们成为“2 x 1a”的循环展开形式。
    针对以上函数,测量的结果如下:
    这里写图片描述

2.10 优化合并代码的结果小结

  • 我们极大化对向量元素加或者乘的函数性能的努力获得了成功,下面总结了对于标量代码所获得的结果:
    这里写图片描述
  • 使用多项优化技术,我们获得到的程序性能已经接近0.50 和1.00的吞吐量界限,只受限与功能单元的容量。与原始代码相比已提升10~20倍,且使用普通的C代码和标准编译器就获得了所有这些改进。此例子说明现代处理器具有相当的计算能力,但是我们可能需要按非常程式化的方式来编写程序以便将这些能力诱发出来。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值