簡體   English   中英

嵌套循環中的並行代碼

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM