【原创】循环体并行优化(一)——循环迭代空间的仿射变换

      最近痴迷于编译原理而不能自拔,这么多年来一直是会在某一段时间痴迷于一件事物,性格使然,无法自控。随着钻研深入,多年前的一个模糊的想法——搞一个自带针对多处理器系统并行优化的编译器,慢慢的发现这个想法并不是遥不可及了。在看了编译原理第二版的并行性和局部性优化章节的内容后,眼前突然豁然开朗。

在多核处理器横行的今天,我的手机上都有八个可以同时执行任务的处理器的年代,很难想象不用并行处理如何去榨取这些硬件的“剩余价值”。甚至手机处理器上的GPU也能够作为并行流处理器使用的情况下,可以毫不夸张的说,处理器资源已经是唾手可得了,如果再算上笔记本电脑、PC上的CPU、GPU的话,可以毫不夸张的说几乎每个人手头的电子设备都是一个小型的“超算中心”,因此研究并行计算技术不再是过去大中型机才需要去面对的课题,而是几乎每个软件系统都必须严肃认真考虑的问题。

其实从我使用第一台超线程处理器的PC机算起(在此非常感谢当年我供职那家公司,为我提供了当时几乎最好的开发硬件资源。),这十多年来我一直都对并行计算和如何最大化利用这些多处理器系统的课题抱有浓厚的兴趣,只是一直不得其门而入,看了些资料,甚至自己也基于DirectX的DirectComputer Shader制作了一套并行计算技术开发的网络视频教程,然而美中不足的是理论多,实际内容少,或者如网友批评指出的那样缺少“干货”。

现在随着学习的深入,总算找到入门之法了,学习有所收获的情况下,不敢独享,遂成此文,算是给大家补些“干货”。另一方面也是因为自己新的收获,兴奋之余,与大家做些分享。这篇文章算是第一篇吧,想把这个在书上只写了两段文字并且只举了一个例子的算法,立刻分享给大家,争取把自己的理解一并分享给大家,让大家轻松掌握这一“技能”。随后我可能不定期更新此专题系列的文章,并且都放在并行计算的专题下,请大家关注。当然我的功力十分有限,能驾驭和分享到什么程度,自己也没底,还请大家多多斧正。

言归正传,这篇文章的标题中有一个重要的概念就是循环迭代空间,首先就让我们来了解这是个“什么鬼”。话说,并行优化一般的程序,或者说一般的程序要利用多处理器的资源做并行优化,第一个重要的突破口就是从程序中的循环做起,尤其是针对大规模数组做循环操做的循环体(多数有多层循环嵌套)开始入手,因为一般的串行程序中的串行部分内在就具有一些固有的串行本质,其实并不适合“细粒度”的并行优化,而只能利用如“多任务(多进程)“、“多线程”等“粗粒度”的并行优化。所以由此也可以看出,这篇文章中(或者说此系列文章)说的并行优化特指语句、指令等微观级别的“细粒度”并行优化。其实另一方面,很多能够用循环搞定的程序,我们广大聪明的程序员们其实都已经做成循环了,因此想改造现有的一些串行程序变成循环体结构,再利用并行优化的可能性已经不大了,所以本文直接从循环体开始聊起,不再涉及如何将一些串行程序改成循环体的话题。

从循环体入手进行并行优化,还有一个天生的优势,那是因为我们现在所知的大多数算法核心部分都是基于循环迭代的方法,其中大部分算法还具有操作大规模数组的本质,比如我们熟悉的FFT算法,因此对循环体进行并行优化还具有并行优化(加速)算法的现实意义。

这样我们就清楚我们今天要聊话题的核心了“操作大规模数组的循环体”,这里其实也就是两个主要客体:循环体和数组。按照书上教条式的说法,一个循环嵌套结构的迭代空间被定义为该嵌套结构中所有循环下标变量取值的组合。这句话其实数学味道很浓,其中第一个要了解的概念就是什么是“空间”,此处的空间其实指的是数学意义上的空间,简单的理解就是向量的集合。那么什么东西构成了这个迭代空间中的向量呢?那就是每一个循环变量的向量化组合。比如下面的循环体:

for(i = 0; i < 100;i++)

     for(j=i;j<100;j++)

     {

          ......

}

其中的循环变量i、j的组合(i,j)就构成了这个循环体迭代空间中的一个二维向量,如果再加上此例中i、j的上下限中所有可能的取值的组合,那么就完整的构成了这个循环体的迭代空间。这里需要注意的是,在一般的程序中我们使用的循环迭代变量基本都是整型,因此这个空间就是由一堆整数组合组成的整型空间。注意有些循环体变量是有界的,比如常见的for循环体中,测试条件表达式一般都规定了循环的上限或下限,这时我们说这个空间是有限的空间,但有些循环比如空条件的for(;;)循环、while循环或者do...while循环等,如果没有明确的在语句中标明循环上下限时,在分析时通常就认为是无限的空间。当然不论是有限的还是无限的循环迭代空间,都是可数的空间。

      另一方面,在上面的例子中有两个循环变量,那么就构成了一个二维的整型(整数)向量空间,当然以此类推,一个循环变量的循环体就构成了一维的向量空间,n个循环变量就构成了n维向量空间。这里还要注意一个细节问题,不是说有几个明确的循环语句就构成几维的循环迭代向量空间,还要注意哪些隐含的循环变量,比如:

 

      while(...)

{

      ......

      i +=1;

      j += 2*i;

}

其中就含有两个隐含的循环变量i、j因此分析时就要分辨出这种情况,从而正确理解有多少维的向量。

      接下来还需要理解的就是循环迭代的向量空间的“离散性”,因为我们说一般的循环变量都是整型(整数)值,因此循环迭代向量空间中的向量,一般用“离散的点”来表示,比如:(i,j)={(0,0),(0,1),(0,2),(1,0),(1,1).....}。如果你具有高中水平的数学知识的话,这些点如何在坐标平面上表示,就不是问题了,可以自己动手画一下。

理解了这些“离散的点”,那么下一个重要的概念就是n维空间中的凸多面体了。一般来说,我们知道循环变量都有上下限,比如之前例子中的i,j,就有上下限:0≤i<100、i≤j<100,如果以这些不等式为边,那么整个空间就落在以这些边为界的凸多面体内,前面例子中的i,j就落在了一个平面凸多边形内。搞明白的同学可以利用高中的解析几何知识试着在平面坐标系中画出这个凸多边形。并且试着画一些符合不等式条件的点,看看这些点是不是都落在了这个凸多边形内(提示0≤i<100、i≤j<100组成的凸多边形看上去是个三角形)。

此处要注意的就是,并不是所有的循环变量及上下限组合都能构成一个n维凸多面体,它们还有可能组成空的集合。也即,循环跌代体根本就不可能被执行。也就通常数学上说的矛盾不等式构成了该凸多面体的边,从而也就不可能存在这样的空间,比如:10<i<1,这样的矛盾不等式,也就是上下限本身就是矛盾的,当然更高维的矛盾不等式,并不像这样的一目了然。

如果你顺利的看到这里,明白了我在说什么,那么恭喜你,并行优化的基础你已经理解一些了。当弄明白了循环迭代空间之后,那么来思考一个问题,既这个循环迭代空间中的点是不是具有先天的顺序关系?恰如我们在循环语句中定义的那样,比如,i,j的取值必须从下限遍历到上限?ok,其实这个顺序就是我们写程序时强加的顺序了,因为如果你顺利的画出了凸多面体的图,那么你就会发现其中的那些点并没有什么“必然的先后顺序”可言,而这种没有顺序的点,意味着我们几乎可以“同时访问“这个空间中的点,也就是迭代空间中的向量。举个简单的例子来说明,比如:

for(i=0;i<10;i++)

{

     a[i]=0;

}

这里的循环迭代空间是一维的,其中的点有10个(0,1,2,.....9),综合我们之前的一系列说法,可以发现这10个点是可以同时访问的,这时假设你的系统中有10个处理器,那么ok,你就可以为每个处理器编个对应的从0-9的一个编号,然后让处理器按自己的编号访问数组元素a[i],并使其值成为0。此时你最终发现这个简单的例子其实并不用执行10次,而是只需要10个处理器“同时执行一次”即可,理论上只需要使用一个处理器循环执行的十分之一的时间即可完成任务。这也是我们用并行计算优化程序的全部意义,同时执行多个任务,而不是依次执行一串的任务,从而节约时间。

当然对于高于一维的情况,就需要为每个处理器按照迭代空间中的向量点坐标来编号,当可用处理器数量比向量点数量少时,就可以为每个处理器重复不同的向量编号,这也就是我们之前讲的迭代空间基本都是可数的全部的意义(可数和无限并不矛盾,这需要你有敏锐的数学思维,不理解的话折回头好好学习下集合论的相关知识)。

到这里如果你都理解了前面说的这些概念性甚至理论性的东西,并认为原来并行优化就这么简单的话,那么请先不要沾沾自喜,其实这里忽略了很多东西,我所举的情况其实有些过于简化和理想化了。大多数情况下,实际代码中的循环体都是异常复杂的,甚至有些向量是冲突的,比如在不同的循环次序中访问到同一数组元素,有的操作是写、有的操作是读,而此时读写的先后顺序是异常重要的,否则就会发生大多数并行计算中都会碰到的“脏读”问题,这时整个程序其实都已经执行错误了,也就谈不上什么优化了。这种情形下你就不能简单的利用并行操作来处理数组元素了。但不要灰心,方法总比问题多,坚持学下去,办法总会有的。

关于迭代空间本身我们就讨论到这里,希望你明白了一些东西,不明白的话补补数学知识,然后再来跟帖提问。接下来我们就要进入此文的核心话题,既迭代空间的仿射变换上了。仿射变换也是数学概念,简单的理解它就是一个线性变换的超集,首先我们接着看下书上的说法:

仿射变换在几何上定义为两个向量空间之间的一个仿射变换或者仿射映射由一个线性变换(运用一次函数进行的变换)接上一个平移组成。

代数的理解既:

对于一个向量空间X中的点x(x1,x2,.......,xn)变换到向量空间Y中的对应点y(y1,y2,.......yn)用如下公式表示:

yi = cn*xn+cn-1*xn-1......c1*x1+ c0

式中ci为待定常数,c0代表了位移分量,c0≠0时也被称为非齐次线性变换。

对于纯线性变换来说c0=0。由此也可以知道仿射变换包含了线性变换。这里如果某个具体函数f(x)完成了上述变换(其实就是f(x)确定了一组ci常数,因此具体的f(x)可以有无穷多个),就被称为仿射函数。
      ok,希望你没有被这里相对简单的数学知识搞蒙。仿射变换在我们并行优化中最大的用处就是用于变换循环迭代空间到连续的空间上。简单的说,如下的循环:

for(i=10;i<=1000;i+=7)

{

     a[i]=......;

}

这个循环并不像之前我们看到的循环那样,i变量是“跳跃式”变换的,每次增量是7,在实际代码中循环变量的情况可能更复杂,这对我们分析迭代空间带来了极大的不便,因为空间的“不连续”,所以我们甚至没法简单的写出这个循环变量的上下限不等式,不要以为10≤i≤1000就是对的。这时我们就需要仿射函数帮我们了,因为我们需要把这个”不连续”空间中的i,变换到“连续”空间中的j,此时我们假设有:

i=7*j+10;

于是j=0时,i=10,符合初始条件,i≤1000,意味着7*j+10≤1000,解出j≤141.4285......,因为我们的迭代空间是整数向量空间,因此对常数结果下取整得到j≤141,然后将原循环改写为:

for(j=0;j<=141;j++)

{

     a[7*j+10]=......;

}

这时,我们看到j已经是“连续”的变换了。当然这是否正确需要我们来验证一下,通过计算几组循环下标比较一下就清楚了:

(原创)循环体并行优化(一)——循环迭代空间的仿射变换 - Gamebaby Rock Sun - Gamebaby Rock Sun的博客

省略部分下标变量计算交给大家来完成,要注意的是,i实际最大只能跳跃到997,而到不了1000,因此i≤1000的意义其实就是i<1000,因为1000不是7的倍数,也不是7*j+10的倍数。

可以再看一个例子:

for(i=-3;i<50;i+=2) a[i]=......

我们取:i=2*j-3得到j<26,j=0时i=-3,改写循环如下:

for(j=0;j<26;i++) a[2*j-3]=......

这里的验证下标的工作就交给各位看客自行完成了,用笔计算了。

那么经过这样了个看着有点神奇的例子,不知道大家对选取仿射函数有没有发现什么窍门了?我相信聪明的你已经看出其中的端倪了,恭喜你可能猜对了,那就是将循环增量取为常量系数c1,循环初始量取为位移量c0,然后对循环变量做变换i=c1*j+c0,接着用i的上限不等式反解出j的上限Nnew,并且下取整,改写循环为for(j=0;j<Nnew;j++),然后替换所有的迭代变量i为c1*j+c0,并简化表达式,得到新的数组下标表达式。

掌握了新方法,就可以试着做做下面的这些练习,彻底掌握这个算法:

for(i =1000;i >=50;i--)

for(i =1000;i >=50;i-=3)

for(i =-7;i <40;i+=3)

提示:递减量取为负系数;

接着可以看看下面的循环:

for(i=10;i>0;i = 70-i)

思考下这个能不能做刚才类似的变换,问题出在哪?

至此关于一维循环迭代空间的仿射变换及如何将 “非连续”迭代空间变换到“连续”迭代空间的方法,就讲完了,这篇短文也就暂告一段落了。关于高维迭代空间的“连续”仿射变换方法,将在下一篇文章中做讲解,因为要用到矩阵的知识,所以请大家做好复习矩阵乘法,以及线性方程矩阵解法等方面的数学知识,当然我也要去好好复习下,以免错过什么。

 

发布了94 篇原创文章 · 获赞 89 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/u014038143/article/details/78192231