F#:告诉我使用Async.Parallel时我缺少什么

时间:2010-04-20 21:37:20

标签: f# parallel-processing

好的,所以我正在做ProjectEuler问题#14,我正在摆弄优化以便感受f#out。

在以下代码中:

let evenrule n = n / 2L
let oddrule n = 3L * n + 1L

let applyRule n =
    if n % 2L = 0L then evenrule n
    else oddrule n

let runRules n =
    let rec loop a final =
        if a = 1L then final
        else loop (applyRule a) (final + 1L)
    n, loop (int64 n) 1L


let testlist = seq {for i in 3 .. 2 .. 1000000 do yield i } 

let getAns sq = sq |> Seq.head

let seqfil (a,acc) (b,curr) = if acc = curr then (a,acc) else if acc < curr then (b,curr) else (a,acc)

let pmap f l = 
    seq { for a in l do yield async {return f a} }
    |> Seq.map Async.RunSynchronously

let pmap2 f l = 
    seq { for a in l do yield async {return f a} }
    |> Async.Parallel
    |> Async.RunSynchronously

let procseq f l = l
                  |> f runRules
                  |> Seq.reduce seqfil
                  |> fst

let timer = System.Diagnostics.Stopwatch()
timer.Start()
let ans1 = testlist |> procseq Seq.map // 837799    00:00:08.6251990
printfn "%A\t%A" ans1 timer.Elapsed
timer.Reset()

timer.Start()
let ans2 = testlist |> procseq pmap
printfn "%A\t%A" ans2 timer.Elapsed // 837799   00:00:12.3010250
timer.Reset()

timer.Start()
let ans3 = testlist |> procseq pmap2
printfn "%A\t%A" ans3 timer.Elapsed // 837799   00:00:58.2413990
timer.Reset()

为什么与直接映射相比,Async.Parallel代码运行速度非常慢?我知道我不应该看到那么大的影响,因为我只是在双核心Mac上。

请注意,我不想帮助解决问题#14,我只是想知道我的并行代码是什么。

3 个答案:

答案 0 :(得分:9)

使用Async.Parallel似乎是正确的。这些数字看起来确实很可疑,但我不会立即看到这里可能存在的问题。

在任何情况下,异步工作流程实际上更适合涉及某些异步操作的计算(例如I / O,通信,等待事件等)。对于CPU密集型任务,最好使用.NET的Parallel Extensions(现在是.NET 4.0的一部分;不幸的是,没有.NET 2.0版本)。

要从F#执行此操作,您需要F# PowerPackFSharp.PowerPack.Parallel.Seq.dll程序集,其中包含用于处理序列的高阶函数的并行版本(例如map: - ))

这些函数返回类型为pseq<'a>的值(在C#中称为ParallelQuery<T>),它表示并行运行的延迟计算(这可以在管道中使用多个操作时实现更好的优化)。还有PSeq.reduce功能,因此您可能也希望在处理中使用此功能(除了PSeq.map)。

答案 1 :(得分:3)

在我的盒子上,运行非异步的大约需要半秒钟。由于这里有大约50万个工作项,这意味着每个工作项的平均运行时间约为1微秒。

这些项目太小,无法单独进行并行化。执行线程调度和同步的开销将主导实际工作。您需要选择更好的粒度。

(我会处理一些快速代码......)

编辑:

好的,代码原样并不是很容易重做,以便在没有重大改写的情况下更改批量粒度,因此我没有新的代码可以分享,而不会给出太多的分析。但是我能够让它在我的2核盒子上的运行速度几乎快两倍,每个批次生成50,000个元素,并在每个批次上进行“map reduce”,在10个批次上进行“parallel-map reduce”。

另见

http://blogs.msdn.com/pfxteam/archive/2008/08/12/8849984.aspx

尤其是“任务粒度”部分。

答案 2 :(得分:0)

  

我只是想知道我的并行代码是什么

嘿,您的并行代码没有错。 ; - )

以下是我如何解决问题:

  let rec inside (a : _ array) n =
    if n <= 1L || a.[int n] > 0s then a.[int n] else
      let p =
        if n &&& 1L = 0L then inside a (n >>> 1) else
          let n = 3L*n + 1L
          if n < int64 a.Length then inside a n else outside a n
      a.[int n] <- 1s + p
      1s + p
  and outside (a : _ array) n =
    let n = if n &&& 1L = 0L then n >>> 1 else 3L*n + 1L
    1s + if n < int64 a.Length then inside a n else outside a n

  let euler14 n =
    let a = Array.create (n+1) 0s
    let a = Array.Parallel.init (n+1) (fun n -> inside a (int64 n))
    let i = Array.findIndex (Array.reduce max a |> (=)) a
    i, a.[i]

该程序使用具有安全竞争条件的推测并行性,并在我的8核上实现适度的2倍加速。