洋洋灑灑寫了5篇博客之後,嘗試把理論用於實際,遇到了一些問題。本文是對實戰中遇到問題的記錄和思考。爲了方便理解,把代碼做了簡化,原理不變。不定時更新,每次更新的日期和內容排在最前面,太長了會分爲多篇文章。
爲敘述簡便,把@everywhere
廣播的對象簡稱爲”廣播對象“,例如:廣播變量、廣播函數等。讀者應該已經理解”共享“和”廣播“之間的區別。另外,@time
第一次運行時會比較慢,要多運行幾遍。
Jun 24, 2019
今天測試後發現,如果在並行化的循環體中引用了結構體數組,耗時會增大兩個數量級。結論是並行程序裏儘量不要用結構體數組。
Jun 23, 2019
我想做這樣一件事:創建一個共享數組W
,在一個函數f
中並行修改W
。代碼如下:
using Distributed
using SharedArrays
addprocs(4-nprocs())
println("Running ",nprocs()," processes")
t = 2. ; nx=1000; ny=1000
W = SharedArray{Float64}((nx,ny),init=A->(A=zeros(nx,ny)))
function f()
@time @sync @distributed for i=1:nx
for j=1:ny
W[i,j] = W[i,j] + t
end
end
end
f()
rmprocs(2,3,4)
逐行解釋這段代碼——
addprocs(4-nprocs())
默認nprocs()=1
,所以是開啓3個遠程Worker。此時打開任務管理器,會看到:
第一個是常駐的終端,不參與運算。第二個是主進程,不論是否並行,都會在有計算任務時啓動。剩下三個是遠程Worker。
t = 2. ; nx=1000; ny=1000
聲明三個變量,第一個加了小數點,令Julia自動識別爲浮點數。
W = SharedArray{Float64}((nx,ny),init=A->(A=zeros(nx,ny)))
聲明一個共享數組並初始化爲零,默認存儲在主進程上,所以在後面的並行計算中,盯着任務管理器會看到主進程完成得比三個遠程Worker更快些。如圖:
function f()
@time @sync @distributed for i=1:nx
for j=1:ny
W[i,j] = W[i,j] + t
end
end
end
這個函數有好幾點需要解釋:
f
可以不帶參數,此時所有變量和數組都自動繼承到函數的局部域中。若帶有參數,則該參數會遵循正常的參數傳遞方式,其餘仍自動繼承。例如修改爲:
function f(nx)
for i=1:nx
for j=1:ny
W[i,j] = W[i,j] + t
end
end
end
W1 = f(nx)
W2 = f(nx+1)
會看到W1
正常運行,而W2
報錯。
- 回到原先的代碼中。
@distributed
已經介紹過了。Julia的多層for循環可以簡寫爲for i=1:m, j=1:n, k=1:p
形式,但@distributed
只能識別最外面一層,所以必須把最外層和裏面幾層拆開,寫成:
@distributed for i=1:m
for j=1:n, k=1:p
<Expr>
end
end
由於Julia對數組是按列讀取的,也即,遍歷第一個指標i
的速度顯著快於其他指標,所以最好把指標i
放在最內側,並行時優先分割其他指標而保持指標i
的完整性。修改爲:
function f()
@time @sync @distributed for j=1:ny
for i=1:nx
W[i,j] = W[i,j] + t
end
end
end
f()
原代碼消耗0.316919 seconds (185.12 k allocations: 9.060 MiB)
,修改後消耗0.262387 seconds (185.10 k allocations: 9.058 MiB)
,可見內存消耗幾乎一致,但速度更快了。
- 在設計並行程序時,會很自然地想到是否要使用廣播變量。上述例子表明:位於
@distributed
結構的三個不同位置的變量nx, ny, t
都不需要廣播,所以在@distributed
裏不被多進程修改的變量不需要廣播。那麼假如要修改變量,應不應當廣播呢?我們來看下面的例子:
function f()
@time @sync @distributed for j=1:ny
for i=1:nx
@everywhere t += 1
W[i,j] = W[i,j] + t
end
end
end
f()
假如去掉其中的@everywhere
就會報錯,證明廣播是可行的。然而,你會發現計算時間大幅延長,因爲@everywhere
是一個遠程調用命令,反覆執行它是很耗時間的。如果你把它挪到前面,像這樣:
@everywhere t = 2
function f()
@time @sync @distributed for j=1:ny
for i=1:nx
t += 1
W[i,j] = W[i,j] + t
end
end
end
f()
系統會報錯。那麼到底怎樣纔是合理的方法?答案是把t
改爲參數,像這樣:
function f(t)
@time @sync @distributed for j=1:ny
for i=1:nx
t += 1
W[i,j] = W[i,j] + t
end
end
end
f(t)
會看到計算時間增加得很少。這種做法不需要廣播。但是!!!當你打印W
時會發現計算結果變了。舉例來說,假如你希望一致地修改t
後加到W
上,以nx=3; ny=2
的情況爲例,得到的W
是:
julia> W
3×2 SharedArray{Float64,2}:
3.0 3.0
3.0 3.0
3.0 3.0
如果用上面的傳參方法會得到:
julia> W
3×2 SharedArray{Float64,2}:
3.0 3.0
4.0 4.0
5.0 5.0
表明t
的修改沿着指標i
的維度被疊加了。而用@everywhere t+=1
方法會得到:
julia> W
3×2 SharedArray{Float64,2}:
4.0 4.0
6.0 6.0
8.0 8.0
這就更誇張了,t
的修改每次在i
方向疊加時還在j
方向上也疊加了一遍。問題出在哪兒呢?很明顯,是表達式的位置有問題,改成下面這樣:
function f2(t)
@time @sync @distributed for j=1:ny
t+=1
for i=1:nx
W[i,j] = W[i,j] + t
end
end
end
獲得了正確的結果。結論是:@distributed
結構的最外層循環是分割開的,互相獨立,變量修改不會疊加。但裏層循環依舊是按照一般程序的循環規則,會把修改進行疊加。使用@distributed
時要記牢這一點。
@sync
是爲了確保各進程全部完成任務後才繼續往下走(下一個語句是@time
)。如果不加@sync
,會看到@time
迅速返回了一個結果0.012252 seconds (8.00 k allocations: 421.867 KiB)
,此時任務管理器裏各進程還在計算中,可見@time
返回的是隻是發起遠程調用的消耗。把nx
和ny
設定得更大些會看得更明顯。如果在一部分進程尚未完成計算的情況下打印W
,會看到它的一部分元素沒有改變。由於共享數組默認存儲在主進程上,一般主進程會率先完成,隨後其餘進程幾乎同時完成,如圖:
rmprocs(2,3,4)
最後,必須像這樣關閉多餘的進程,否則再次執行代碼時系統會爲已有的進程增加新的內存,導致佔用內存增加一倍,多運行幾次就喫不消了。關閉遠程進程後如下圖:
但記得不要試圖關閉主進程,那樣系統會拒絕執行rmprocs()
命令。