[英]Parallelize code in nested loops
您總是會聽到功能代碼本質上比非功能代碼更容易並行化,因此我決定編寫一個執行以下操作的函數:
給定一個字符串輸入,每個字符串的唯一字符總數。 因此,給定輸入[ "aaaaa"; "bbb"; "ccccccc"; "abbbc" ]
[ "aaaaa"; "bbb"; "ccccccc"; "abbbc" ]
[ "aaaaa"; "bbb"; "ccccccc"; "abbbc" ]
,我們的方法將返回a: 6; b: 6; c: 8
a: 6; b: 6; c: 8
a: 6; b: 6; c: 8
。
這是我寫的:
(* seq<#seq<char>> -> Map<char,int> *)
let wordFrequency input =
input
|> Seq.fold (fun acc text ->
(* This inner loop can be processed on its own thread *)
text
|> Seq.choose (fun char -> if Char.IsLetter char then Some(char) else None)
|> Seq.fold (fun (acc : Map<_,_>) item ->
match acc.TryFind(item) with
| Some(count) -> acc.Add(item, count + 1)
| None -> acc.Add(item, 1))
acc
) Map.empty
該代碼在理論上是可並行化的,因為input
每個字符串都可以在其自己的線程上進行處理。 它不像看起來那樣簡單,因為innerloop將項目添加到所有輸入之間共享的Map中。
我希望將內部循環分解成自己的線程,並且我不想使用任何可變狀態。 如何使用異步工作流程重新編寫此功能?
您可以這樣寫:
let wordFrequency =
Seq.concat >> Seq.filter System.Char.IsLetter >> Seq.countBy id >> Map.ofSeq
並僅用兩個額外的字符對其進行並行處理,以使用FSharp.PowerPack.Parallel.Seq
DLL中的PSeq
模塊而不是普通的Seq
模塊:
let wordFrequency =
Seq.concat >> PSeq.filter System.Char.IsLetter >> PSeq.countBy id >> Map.ofSeq
例如,從5.5Mb詹姆斯國王聖經計算頻率所需的時間從4.75s下降到0.66s。 這是這台8核機器的7.2倍加速。
如前所述,如果您嘗試讓不同的線程處理不同的輸入字符串,則存在更新爭用,因為每個線程都可以增加每個字母的計數。 您可以讓每個線程生成自己的Map,然后“加總所有Map”,但是最后一步可能很昂貴(由於共享數據,因此不適合使用線程)。 我認為使用以下算法,大型輸入可能會更快地運行,其中每個線程處理不同的字母到計數(對於輸入中的所有字符串)。 結果,每個線程都有其自己的獨立計數器,因此沒有更新爭用,也沒有合並結果的最后一步。 但是,我們需要進行預處理以發現“唯一字母集”,並且此步驟確實存在相同的爭用問題。 (在實踐中,您可能預先知道字符的范圍,例如字母,然后只能創建26個線程來處理az,並繞過此問題。)在任何情況下,大概的問題都在於探索“如何編寫F#”。異步代碼將工作划分為多個線程,因此下面的代碼對此進行了演示。
#light
let input = [| "aaaaa"; "bbb"; "ccccccc"; "abbbc" |]
// first discover all unique letters used
let Letters str =
str |> Seq.fold (fun set c -> Set.add c set) Set.empty
let allLetters =
input |> Array.map (fun str ->
async { return Letters str })
|> Async.Parallel
|> Async.Run
|> Set.union_all // note, this step is single-threaded,
// if input has many strings, can improve this
// Now count each letter on a separate thread
let CountLetter letter =
let mutable count = 0
for str in input do
for c in str do
if letter = c then
count <- count + 1
letter, count
let result =
allLetters |> Seq.map (fun c ->
async { return CountLetter c })
|> Async.Parallel
|> Async.Run
// print results
for letter,count in result do
printfn "%c : %d" letter count
我確實已經“徹底改變了算法”,主要是因為由於更新爭用,您原來使用的算法並不特別適合直接數據並行化。 根據您要學習的內容,此答案可能對您特別滿意,也可能不滿意。
正如Don Syme解釋的那樣,並行與異步並不相同。
因此,IMO您最好使用PLINQ進行並行化。
我一點也不講F#,但是我可以解決這個問題。 考慮使用map / reduce:
令n = card(Σ)為字母Σ中符號的數量σ。
地圖階段:
產卵數處理,其中第i個過程的分配是相符符號的出現次數σi在全部輸入向量。
還原階段 :
依次收集n個進程中每個進程的總數。 該向量是您的結果。
現在,此版本不會比串行版本產生任何改進。 我懷疑這里有一個隱藏的依賴關系,這使得它本來就很難並行化,但是我太累了,腦子死了,無法今晚證明這一點。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.