计算完美数字时F#并行化问题?

时间:2011-12-03 13:53:41

标签: f# parallel-processing discrete-mathematics perfect-numbers

我正在尝试优化一个小程序,它可以计算给定指数的完美数字。

程序运行(几乎)完美,但是当我打开任务管理器时,它仍然在单个线程上运行。这意味着我必须做错事,但我对F#的了解还处于“开始”阶段。

我会尽可能明确地提出这个问题,但如果我这样做不成功,请告诉我。

完美数字是一个数字,其中所有除数的总和(数字本身除外)等于数字本身(例如6是完美的,因为它的除数1,2和3的总和是6)。

我使用素数来加速计算,即我对存储所有除数的(巨大)列表不感兴趣。为此,我使用欧几里德证明是正确的公式:(2 *(数字-1的幂))*(2 *(数字-1的幂))其中后者是梅森素数。我使用了stackoverflow(由@Juliet)提供的一种非常快速的算法来确定给定的数字是否为素数。

由于我在互联网上阅读了几篇文章(我还没有购买好书,对我这么羞耻),我发现序列比列表表现更好。这就是为什么我首先开始创建一个生成一系列完美数字的函数:

   let perfectNumbersTwo (n : int) =  
    seq { for i in 1..n do 
           if (PowShift i) - 1I |> isPrime 
           then yield PowShift (i-1) * ((PowShift i)-1I)
        } 

辅助功能PowShift实现如下:

    let inline PowShift (exp:int32) = 1I <<< exp ;;

我使用位移算子,因为所有功率计算的基数都是2,因此这可能是一种简单的方法。当然,我仍然感谢我就这个问题所做的贡献:F#Power问题接受两个参数都是bigints&gt; F# Power issues which accepts both arguments to be bigints

Juliet创建的函数(borrowed here)如下:

let isPrime ( n : bigint) = 
    let maxFactor = bigint(sqrt(float n))
    let rec loop testPrime tog =
        if testPrime > maxFactor then true
        elif n % testPrime = 0I then false
        else loop (testPrime + tog) (6I - tog)
    if n = 2I || n = 3I || n = 5I then true
    elif n <= 1I || n % 2I = 0I || n % 3I = 0I || n % 5I = 0I then false
    else loop 7I 4I;;

使用此代码,没有并行,我的笔记本电脑上需要大约9分钟才能找到第9个完美数字(由37位数字组成,并且可以找到值为31的指数)。由于我的笔记本电脑有一个带有两个内核的CPU,而且只有一个内存为50%(一个内核满载),我可以通过并行计算结果来加速计算。

所以我改变了我的perfectnumber函数如下:

//Now the function again, but async for parallel computing
let perfectNumbersAsync ( n : int) =
    async {
        try
            for x in 1.. n do
                if PowShift x - 1I |> isPrime then
                    let result = PowShift (x-1) * ((PowShift x)-1I)
                    printfn "Found %A as a perfect number" result
        with
            | ex -> printfn "Error%s" (ex.Message);
    }

要调用此函数,我使用一个小辅助函数来运行它:

 let runPerfects n =
    [n]
        |> Seq.map perfectNumbersAsync
        |> Async.Parallel
        |> Async.RunSynchronously
        |> ignore

忽略异步计算的结果,因为我在其中显示它 perfectNumbersAsync函数。

上面的代码编译并运行,但它仍然只使用一个核心(虽然它在计算第9个完美数字时运行速度快10秒)。我担心它必须用辅助函数PowShift和isPrime做一些事情,但我不确定。我是否必须将这些辅助函数的代码放在perfectNumbersAsync的异步块中?它没有提高可读性......

我玩F#的次数越多,我就越能学会欣赏这种语言,但就这种情况而言,我有时需要一些专家:)。

提前感谢您阅读本文,我只希望自己有点清楚......

罗伯特。

2 个答案:

答案 0 :(得分:3)

@Jeffrey Sax的评论非常有趣,所以我花了一些时间做一个小实验。 Lucas-Lehmer测试编写如下:

let lucasLehmer p =
    let m = (PowShift p) - 1I
    let rec loop i acc =
        if i = p-2 then acc
        else loop (i+1) ((acc*acc - 2I)%m)
    (loop 0 4I) = 0I

通过Lucas-Lehmer测试,我可以非常快速地得到几个完美的数字:

let mersenne (i: int) =     
    if i = 2 || (isPrime (bigint i) && lucasLehmer i) then
        let p = PowShift i
        Some ((p/2I) * (p-1I))
    else None

let runPerfects n =
    seq [1..n]
        |> Seq.choose mersenne
        |> Seq.toArray

let m1 = runPerfects 2048;; // Real: 00:00:07.839, CPU: 00:00:07.878, GC gen0: 112, gen1: 2, gen2: 1

Lucas-Lehmer测试有助于减少检查素数的时间。我们使用最多为O(sqrt(2^p-1))的素性测试,而不是测试O(p^3)的2 ^ p-1的可分性。 使用n = 2048,我能够在7.83秒内找到前15个梅森数。第15个梅森数是i = 1279的数字,它由770个数字组成。

我尝试在F# Powerpack中使用PSeq模块并行化runPerfects。 PSeq不保留原始序列的顺序,所以公平地说我已经对输出序列进行了排序。由于素数测试在指数之间非常平衡,因此结果非常令人鼓舞:

#r "FSharp.Powerpack.Parallel.Seq.dll"    
open Microsoft.FSharp.Collections

let runPerfectsPar n =
    seq [1..n]
        |> PSeq.choose mersenne
        |> PSeq.sort (* align with sequential version *)
        |> PSeq.toArray 

let m2 = runPerfectsPar 2048;; // Real: 00:00:02.288, CPU: 00:00:07.987, GC gen0: 115, gen1: 1, gen2: 0

使用相同的输入,并行版本需要2.28秒,这相当于我的四核机器上的3.4倍加速。我相信如果你使用Parallel.For构造并明智地划分输入范围,结果可以进一步改善。

答案 1 :(得分:3)

快速评论速度和并行性,

你的isPrime是O(sqrt(n)),并且每个成功的n大约是最后一个的2倍,因此计算时需要大约1.5 x,这意味着计算最后一个数字需要更长的时间

我做过一些黑客攻击测试素数,我发现一些有用的东西是:

  1. 对于大N,(您正在测试20位数字),素数密度实际上非常低,因此您将按复合数进行大量分割。 更好的方法是预先计算质数表(使用筛子)达到某个最大限度(可能由内存量决定)。请注意,您最有可能找到数字较小的因子。一旦桌面内存不足,您可以使用现有功能测试其余数字,并使用更大的起点。

  2. 另一种方法是在检查中使用多个线程。例如,您目前检查x,x+4,x+6...作为因素。通过稍微聪明一点,你可以在1个线程中将数字一致到1 mod 3,并且在另一个线程中数字与2 mod 3一致。

  3. 没有。 2是最简单的,但是No.1更有效,并且提供了使用OutOfMemoryExceptions进行控制流程的潜力,这可能总是很有趣

    修改 所以我实现了这两个想法,它几乎立即找到2305843008139952128,找到2658455991569831744654692615953842176在我的电脑上需要7分钟(四核AMD 3200)。大部分时间用于检查2 ^ 61是素数,因此更好的算法可能更适合检查素数:此处代码

    let swatch = new System.Diagnostics.Stopwatch()
    swatch.Start()
    let inline PowShift (exp:int32) = 1I <<< exp ;;
    let limit = 10000000 //go to a limit, makes table gen slow, but should pay off
    printfn "making table"
    //returns an array of all the primes up to limit
    let table =
        let table = Array.create limit true //use bools in the table to save on memory
        let tlimit = int (sqrt (float limit)) //max test no for table, ints should be fine
        table.[1] <- false //special case
        [2..tlimit] 
        |> List.iter (fun t -> 
            if table.[t]  then //simple optimisation
                let mutable v = t*2
                while v < limit do
                    table.[v] <- false
                    v <- v + t)
        let out = Array.create (50847534) 0I //wolfram alpha provides pi(1 billion) - want to minimize memory
        let mutable idx = 0
        for x in [1..(limit-1)] do
            if table.[x] then
                out.[idx] <- bigint x
                idx <- idx + 1
        out |> Array.filter (fun t -> t <> 0I) //wolfram no is for 1 billion as limit, we use a smaller number
    printfn "table made"
    
    let rec isploop testprime incr max n=
        if testprime > max then true
        else if n % testprime = 0I then false
        else isploop (testprime + incr) incr max n
    
    let isPrime ( n : bigint) = 
        //first test the table
        let maxFactor = bigint(sqrt(float n))
        match table |> Array.tryFind (fun t -> n % t = 0I && t <= maxFactor) with
        |Some(t) -> 
            false
        |None -> //now slow test
            //I have 4 cores so
            let bases = [|limit;limit+1;limit+3;limit+4|] //uses the fact that 10^x congruent to 1 mod 3
            //for 2 cores, drop last 2 terms above and change 6I to 3I
            match bases |> Array.map (fun t -> async {return isploop (bigint t) 6I maxFactor n}) |> Async.Parallel |> Async.RunSynchronously |> Array.tryFind (fun t -> t = false) with
            |Some(t) -> false
            |None -> true
    
    
    let pcount = ref 0
    let perfectNumbersTwo (n : int) =  
        seq { for i in 2..n do 
               if (isPrime (bigint i)) then
                    if (PowShift i) - 1I |> isPrime then
                        pcount := !pcount + 1
                        if !pcount = 9 then
                            swatch.Stop()
                            printfn "total time %f seconds, %i:%i m:s"  (swatch.Elapsed.TotalSeconds) (swatch.Elapsed.Minutes) (swatch.Elapsed.Seconds)
                        yield PowShift (i-1) * ((PowShift i)-1I)
            } 
    
    
    perfectNumbersTwo 62 |> Seq.iter (printfn "PERFECT: %A") //62 gives 9th number
    
    printfn "done"
    System.Console.Read() |> ignore