F # параллелизующая проблема при вычислении идеальных чисел?

Я пытаюсь оптимизировать небольшую программу, которая вычисляет совершенные числа из данного показателя.

Программа работает (почти) отлично, но когда я открываю диспетчер задач, она по-прежнему работает на одном потоке. Это означает, что я должен делать что-то неправильно, но мое знание F # все еще находится в начальной стадии.

Я постараюсь поставить этот вопрос как можно яснее, но если я не сделаю этого, сообщите мне.

Совершенное число - это число, где сумма всех его делителей (кроме самого числа) равна самому числу (например, 6 совершенна, так как сумма его делителей 1, 2 и 3 равна 6).

Я использую простые числа, чтобы ускорить вычисление, то есть меня не интересуют (огромные) списки, где хранятся все делители. Для этого я использую формулу, согласно которой Евклид оказался верным: (2 * (мощность num - 1)) * (2 * (мощность num - 1)), где последнее является простым числом Мерсенна. Я использовал очень быстрый алгоритм из stackoverflow (by @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 > F # Проблемы с питанием, которые принимают оба аргумента как bigints

Созданная функция Juliet (заимствованная здесь) выглядит следующим образом:

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 для экспоненты). Поскольку у моего ноутбука есть процессор с двумя ядрами, и только один работает на 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

Если результат вычисления async игнорируется, поскольку я показываю его в пределах perfectNumbersAsync.

Приведенный выше код компилируется и запускается, однако он по-прежнему использует только одно ядро ​​(хотя при вычислении 9-го совершенного числа он работает на 10 секунд быстрее). Я боюсь, что он должен что-то сделать с вспомогательными функциями PowShift и isPrime, но я не уверен. Должен ли я помещать код этих вспомогательных функций в блок асинхронизации perfectNumbersAsync? Это не улучшает читаемость...

Чем больше я играю с F #, тем больше я учусь ценить этот язык, но, как и в этом случае, мне иногда нужны некоторые эксперты:).

Заранее спасибо за чтение этого, я только надеюсь, что я сделал себе немного ясно...

Роберт.

Ответ 1

@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 помогает сократить время проверки простых чисел. Вместо проверки делимости 2 ^ p-1, которая принимает O(sqrt(2^p-1)), мы используем тест примитивности, который не превосходит O(p^3). С n = 2048 я могу найти первые 15 чисел Мерсенна за 7,83 секунды. 15-е число Мерсенны - это номер с i = 1279 и состоит из 770 цифр.

Я попытался распараллелить runPerfects с помощью модуля PSeq в F # Powerpack. 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 и разделите диапазон ввода разумно.

Ответ 2

Один быстрый комментарий по скорости и параллелизуемости,

Ваш isPrime - это O (sqrt (n)), и каждый succesive n имеет размер примерно 2 x, такой же большой, как и последний, поэтому для вычисления потребуется приблизительно 1,5 x, что означает, что вычисление последних чисел будет занимать гораздо больше времени

Я сделал некоторые взлома с проверкой на простоту, и некоторые вещи, которые я нашел полезными:

  • Для больших N (вы тестируете числа с 20 цифрами), плотность на самом деле довольно низкая, поэтому вы будете делать много делений по составным числам. Лучшим подходом является предварительная калькуляция таблицы простых чисел (с использованием сита) до некоторого максимального предела (вероятно, определяется объемом памяти). Обратите внимание, что вы, скорее всего, найдете факторы с небольшими числами. После того, как у вас закончится нехватка памяти для вашей таблицы, вы можете протестировать остальную часть номера с помощью существующей функции с большей начальной точкой.

  • Другой подход заключается в использовании нескольких потоков в проверке. Например, вы в настоящее время проверяете x,x+4,x+6... как факторы. Чувствуя себя немного умнее, вы можете сделать число, сравнимое с 1 mod 3 в 1 потоке, и числа, совпадающие с 2 mod 3 в другом потоке.

Нет. 2 является простейшим, но № 1 является более эффективным и обеспечивает потенциал для управления потоком с OutOfMemoryExceptions, который всегда может быть интересным

EDIT: Так что я реализовал обе эти идеи, он находит 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