Проблема с распараллеливанием в F # при вычислении точных чисел?

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

Программа работает (почти) идеально, но когда я открываю диспетчер задач, она по-прежнему работает в одном потоке. Это означает, что я, должно быть, делаю что-то не так, но мои знания 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 #, которые принимают оба аргумента как важные> Проблемы F # Power, которые принимают оба аргумента как важные

Функция, созданная Джульеттой (заимствована здесь) выглядит следующим образом:

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

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

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

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

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

Роберт.


person RvdV79    schedule 03.12.2011    source источник


Ответы (2)


Комментарий @Jeffrey Sax определенно интересен, поэтому мне потребовалось время, чтобы провести небольшой эксперимент. Тест Лукаса-Лемера записывается следующим образом:

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

С помощью теста Лукаса-Лемера я могу очень быстро получить несколько первых идеальных чисел:

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

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

person pad    schedule 03.12.2011
comment
Еще раз спасибо, мой друг, ты снова мне помог. Хотя в вашем коде было несколько мелких ошибок (что хорошо, так как предотвращает копирование, не понимая, что вы делаете). У меня есть еще один компьютер с четырьмя ядрами, но вы правы, прирост скорости будет незначительным. Однако я снова узнал: D. - person RvdV79; 04.12.2011
comment
У вас есть ускорение? Что вы сделали после распараллеливания этого примера? Будет интересно увидеть реальные результаты. - person pad; 04.12.2011
comment
Ускорение оказывается минимальным (на 1 секунду быстрее), по-прежнему используется одно ядро, что немного странно ... Может ли это быть вызвано тем, что я использую этот пример в консольном приложении? Я с трудом могу в это поверить ... - person RvdV79; 05.12.2011
comment
@ RvdV79 - здесь используется только 1 ядро, потому что почти все время тратится на проверку простоты 2 ^ 61-1, которая является только однопоточной. - person John Palmer; 05.12.2011
comment
@ RvdV79: Я получил очень хорошее ускорение с помощью теста Лукаса-Лемера и параллельной последовательности. Вы можете посмотреть на это. - person pad; 05.12.2011
comment
@pad Удивительно, я протестировал ваш код, и он оказался невероятно быстрым! Теперь мне нужно время, чтобы изучить его, прежде чем я действительно им воспользуюсь. Еще один (не по теме) вопрос, можете ли вы порекомендовать книги по F #? - person RvdV79; 06.12.2011
comment
@ RvdV79: Функциональное программирование Realworld (rads.stackoverflow.com/amzn/click/1933988924 ) - хорошая книга для изучения F # и концепций функционального программирования. Эксперт F # подходит для справки (amazon.com/dp/1590598504 /? tag = stackoverfl08-20). - person pad; 06.12.2011
comment
Вы действительно начинаете быть моим личным героем :-). Я заказал книгу «Функциональное программирование в реальном мире» (на Bol.com, так как она мне больше подходит), так что я тоже могу немного поддержать @thomaspetricek. У этой книги Expert F # 2.0 здесь, в Нидерландах, и она довольно дорогая (60 евро, что составляет около 80 долларов, если я не ошибаюсь). Я оставлю это моей жене, чтобы решить, будет ли это хорошим рождественским подарком. - person RvdV79; 06.12.2011
comment
Не за что :). Приятно видеть ваш энтузиазм в изучении F #. - person pad; 06.12.2011
comment
@pad Небольшое обновление: вчера я получил книгу «Функциональное программирование в реальном мире». Это действительно очень полезная книга, поэтому еще раз хочу выразить свою благодарность! - person RvdV79; 09.12.2011

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

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

Я проделал несколько хакерских работ с тестированием на простоту и обнаружил некоторые полезные вещи:

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

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

№2 - самый простой, но №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
person John Palmer    schedule 03.12.2011
comment
Спасибо за такой элегантный подход! Он находит это число за 543 секунды на моем Duo Core Centrino (9 минут 03 секунды), что почти так же быстро, как и мое предыдущее решение. Оба ядра усердно работают, так что это доказывает, что это решение работает немного лучше, чем тот @pad, представленный выше. Использование памяти (для хранения таблицы) довольно велико, почти 700 МБ, в то время как мой подход является стабильным (но опять же, он не создает таблицу поиска) и использует 3,8 МБ памяти. Когда найду время, посмотрю, каковы преимущества с точки зрения скорости. - person RvdV79; 05.12.2011
comment
@ RvdV79 - на самом деле в приведенном выше коде есть тривиальная оптимизация, которая сделает его в 2 раза быстрее, что я оставлю вам, чтобы вы его нашли. Вы можете уменьшить таблицу, что, вероятно, не повлияет на время выполнения, что в основном связано с проверкой первичности последнего члена. - person John Palmer; 05.12.2011
comment
Обратите внимание, что я только новичок в F #, изучение кода, который вы разместили выше, напоминает мне кое-что, что я изучал в статье (cs.hmc.edu/~oneill/papers/Sieve-JFP.pdf), который я нашел как рекомендованный также в Stackoverflow. Я тоже не самый блестящий математик, но думаю, что оптимизация, на которую вы указываете, может быть связана с устранением известных факторов 2, 3, 5 и 7. Я прав? Так как это то, что я делал в алгоритме, с которого начал. Я еще немного изучу, поэтому, пожалуйста, не отвечайте сразу, может быть достаточно подсказки :-) - person RvdV79; 05.12.2011
comment
Жалко, что я не могу оставлять более длинные комментарии, так что простите, что я пишу два, которые на самом деле должны были быть одним. Еще один мой недостаток заключается в том, что английский не является моим родным языком, поэтому мне приходится перечитывать текст снова и снова, чтобы понять, что люди на самом деле пишут и имеют в виду. Реакции и ответы здесь даются быстрее, чем я могу справиться (путем ответов и изучения данной информации), однако это не значит, что я всем вам очень благодарен! - person RvdV79; 05.12.2011
comment
Вот подсказка: мы проверяем числа, начиная с [|limit;limit+1;limit+3;limit+4|], с увеличением на 6, но это не обязательно, поскольку некоторые из этих чисел мы можем очень легко исключить как составные. - person John Palmer; 06.12.2011