嵌套的parfor循环和for循环
如下面的代码所示:
Code #4-1
1.parfor i = 1:10
2.MyFun(i)
3.end
4.
5.function MyFun(i)
6.parfor j = 1:5
7. ...
8.end
9.end
代码#4-1为在循环里面调用一个函数,同时该函数也使用parfor做并行计算,然而,在语句parfor i = 1:10执行后,处理器的并行计算就只会落在这条语句,其余后面开启的并行parfor并不会生效。
将嵌套循环转换为parfor时的注意点
下列代码片实现一个典型的双循环二维数组赋值功能,在我的电脑上跑出来的时间约为42.3秒。
Code #4-2
1.A = 100;
2.tic
3.for i = 1:100
4.for j = 1:100
5.a(i,j) = max(abs(eig(rand(A))));
6.end
7.end
8.toc
现在,由我们前文的叙述可知,双圈循环只能改装一个for为parfor实现并行计算,那么,在这一个代码片#4-2中应该改装哪个for才能实现最高效的计算呢,下面给出一种度量方法,在代码片#4-3中加入了输入输出数据量观察ticBytes字眼,并改装第一个for循环为parfor循环。
Code #4-3
1.A = 100;
2.tic
3.ticBytes(gcp);
4.parfor i = 1:100
5.for j = 1:100
6.a(i,j) = max(abs(eig(rand(A))));
7.end
8.end
9.tocBytes(gcp)
toc
在笔者的双核笔记本上跑出来差不多是快了2倍,证明这个parfor的改装是具备较高的效益的,下面我们来看改装另一个parfor的结果代码#4-4。
Code #4-4
1.A = 100;
2.tic
3.ticBytes(gcp);
4.for i = 1:100
5.parfor j = 1:100
6.a(i,j) = max(abs(eig(rand(A))));
7.end
8.end
9.tocBytes(gcp)
toc
跑出来的时间稍微慢了一点,虽然不算很差,认真看我们发现内嵌的parfor循环的数据量比外循环版本大了一个量级,那么为什么会出现这样的差别呢,万能的Matlab官方文档中并没有给出答案,本文作者给出的一个合理的解释如下:
- 使用外层的并行计算,内层做多而数据量不大的循环计算,cpu的物理内核从一开始就分配到了内层的一个整个循环任务,因此计算的效率较高,传入传出的数据量较少;
- 相比较下,使用内层作为parfor并行的话,在任务上的计算是和外层一致的,但一圈j = 1:100计算完后,parfor结束,由外层循环i重新进入parfor循环时,cpu会需要消耗进入parfor模式分配并行,此时是存在开销的;
这个实验证明了频繁启动parfor是有负面影响的,因此改装外层并行比内层快是合理的。
嵌套循环中变量的使用注意事项
观察下面左边的代码片在内层循环中j的上限需要实时计算矩阵A的列数,然而,在数据的处理段同样有读写数组A的行为,因此这样的效率是不高的,如右侧代码片所示,先把矩阵A的列上限求出再进行计算是一个高效的方法。
无效 | 有效 |
A = zeros(100, 200); parfor i = 1:size(A, 1) for j = 1:size(A, 2) A(i, j) = i + j; end end | A = zeros(100, 200); n = size(A, 2); parfor i = 1:size(A,1) for j = 1:n A(i, j) = i + j; end end |
如下面左面代码片的问题是使用隐含的矩阵索引j+1,在并行计算中索引数组时应尽量使用循环i和j的直接索引写矩阵,右代码片所示。
无效 | 有效 |
A = zeros(4, 11); parfor i = 1:4 for j = 1:10 A(i, j + 1) = i + j; end end | A = zeros(4, 11); parfor i = 1:4 for j = 2:11 A(i, j) = i + j - 1; end end |
如下面的代码片所示,若嵌套循环中使用了矩阵A,则不可以在第一层循环中直接读取矩阵A是,因为第一层循环正在并行计算的模式下,右边的代码是合法的,因为其声明了一个临时变量
无效 | 有效 |
A = zeros(4, 10); parfor i = 1:4 for j = 1:10 A(i, j) = i + j; end disp(A(i, 1)) end | A = zeros(4, 10); parfor i = 1:4 v = zeros(1, 10); for j = 1:10 v(j) = i + j; end disp(v(1)) A(i, :) = v; end |
下面左边代码的问题是,在parfor循环的内嵌循环中,索引不允许割裂,右则代码是一个正确的示范,可以允许通过条件分支判断,但循环索引是需要连贯的。
无效 | 有效 |
A = zeros(4, 10); parfor i = 1:4 for j = 1:5 A(i, j) = i + j; end for k = 6:10 A(i, k) = pi; end end | A = zeros(4, 10); parfor i = 1:4 for j = 1:10 if j < 6 A(i, j) = i + j; else A(i, j) = pi; end end end |