嵌套的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 |