自编教材实操课程分享:第六章—循环展开和压紧
先进编译实验室
2024年08月31日 08:00
收录于文集
共28篇

本文主要介绍循环展开和压紧。实验环境是CentOS7 + GCC。

1. 优化方法简述

循环展开是一种常用的提高程序性能的方法,通过将循环体内的代码复制多次的操作,进而减少循环分支指令执行的次数,增大处理器指令调度的空间,获得更多的指令级并行。此外循环展开将迭代间并行转为了迭代内并行,可以在展开后的循环体内发掘数据级并行,生成向量访存和运算指令以提高性能。

循环压紧是指调整复制后的语句执行,将原来一条语句复制得到的多条语句合并到一起。其中对最内层循环进行展开总是合法的,而压紧则要满足语句执行顺序调整正确性的要求。

2. 示例分析

2.1 示例一

下面的代码实现了一个简单的矩阵运算,首先声明了几个变量,以及3个二维数组A、B、C,分别用来存储矩阵的元素,之后使用双层循环初始化三个矩阵的值。接下来再使用双重循环对矩阵A进行遍历,并更新每个元素的值。最后再次使用嵌套循环计算了矩阵A各元素的累加和。代码中的第二个循环是核心计算部分,可以针对当前循环的内层循环进行循环展开。

优化前后的代码对比如下所示:

优化前的代码采用双重循环遍历二维数组,逐个计算矩阵相加和相乘的结果,每次循环都只计算矩阵A的一个元素。优化后的代码在内层循环中对j进行了循环展开,每次循环计算矩阵A的4个元素,代码最后使用了一个尾部循环,来处理无法被4整除的剩余元素。

编译运行命令:

(1) gcc -o3 LoopUnrolling-1-before.c -o LoopUnrolling-1-before

(2)./LoopUnrolling-1-before

(3)gcc -o3 LoopUnrolling-1-after.c -o LoopUnrolling-1-after

(4)./LoopUnrolling-1-after

可以看到优化前后程序的输出结果是一样的,对比优化前后的运行时间,可以发现使用循环展开优化后的程序所用时间更短。

汇编代码生成命令:

(1)gcc -S LoopUnrolling-1-before.c -o LoopUnrolling-1-before.s

(2)gcc -S LoopUnrolling-1-after.c -o LoopUnrolling-1-after.s

2.2 示例二

如果上述所说的计算效果未能达到预期,可以继续对展开后的代码进行进一步优化,可以采用向量化方法对代码进行改写,进一步挖掘数据集的并行性,进一步改写后的代码如下所示:

代码首先定义了用于存储矩阵的4个大小为N * N数组A、B、C、D,同时定义了存储向量的数组d、e、f,每个数组的大小为4,block表示每次循环迭代处理的向量长度。接着通过循环加载向量d、e、f的值,然后将它们存储到数组A、B、C的对应位置上,这个过程可以看作是将一维数组d、e、f转化为二维数组A、B、C的初始化过程,然后执行矩阵的乘法和加法操作,先加载数组A、B、C中对应位置的值,然后使用AVX指令进行乘法和加法操作,并将结果存回数组A中的对应位置。

编译运行命令:

(1) gcc -o3 -mavx LoopUnrolling-2.c -o LoopUnrolling-2

(2)./LoopUnrolling-2

汇编代码生成命令:

(1)gcc -mavx -S LoopUnrolling-2.c -o LoopUnrolling-2.s

汇编代码涉及到了AVX指令集的使用,这些指令主要用于向量化运算,可以同时处理多个数据元素,从而提高浮点数的运算效率。

2.3 示例三

当循环的迭代次数较少时可以对循环进行完全展开,进一步减少循环控制的开销,循环完全展开对循环嵌套的最内层循环或者向量化后的循环加速效果更明显。并不是循环展开的次数越多获得的程序性能越高,过度的进行循环展开可能会增加寄存器的压力,引起指令缓存区的溢出。以示例三为例进行说明。示例三中共有六个函数,每个函数都包含一个循环,用于对一个包含1000000个元素的浮点数数组a进行操作。函数的循环展开次数逐渐增加。

编译运行命令:

(1) gcc -o3 LoopUnrolling-3.c -o LoopUnrolling-3

(2)./LoopUnrolling-3

可以看出,程序展开两次相比未展开时,性能有明显提升;展开次数为4时,性能相比展开次数为2时也有明显提升;当循环展开达到6次、8次时,性能较展开4次明显下降。因此展开因子要选取合适,如果展开次数太多,会造成性能急剧下降。因为展开次数太多时,运算过程的中间变量随之增加,而计算机的寄存器个数是固定的,当变量个数超过寄存器数量,那么变量只能存到栈中,从而导致性能下降。同时循环展开次数过多,会使得程序代码膨胀、代码可读性降低。

3. 总结

循环展开通过将循环体内的代码复制多次的操作,进而减少循环分支指令执行的次数,增大处理器指令调度的空间,获得更多的指令级并行。此外循环展开将迭代间并行转为了迭代内并行,可以在展开后的循环体内发掘数据级并行,生成向量访存和运算指令以提高性能。循环压紧是调整复制后的语句执行,将原来一条语句复制得到的多条语句合并到一起。其中对最内层循环进行展开总是合法的,而压紧则要满足语句执行顺序调整正确性的要求。

4. 参考资料

(1)编译代码性能优化实践:理解循环展开(pragma unroll)_#pragma unroll-CSDN博客

(2)【C/C++ 性能优化】循环展开在C++中的艺术:提升性能的策略与实践-阿里云开发者社区 (aliyun.com)