Можно ли улучшить время выполнения этого генератора простых чисел? - PullRequest
10 голосов
/ 13 января 2010

Моей первоначальной целью при написании этого было оставить как можно меньше места. Я могу с уверенностью сказать, что эта цель была достигнута. К сожалению, это оставляет меня с довольно медленной реализацией. Для генерации всех простых чисел менее 2 миллионов требуется около 8 секунд на чипе Intel с тактовой частотой 3 ГГц.

Есть ли способ улучшить время выполнения этого кода с минимальным ущербом для небольшого объема памяти? Или я поступаю неправильно, если смотреть на это с функциональной точки зрения?

КОД

/// 6.5s for max = 2,000,000
let generatePrimeNumbers max =    
    let rec generate number numberSequence =
        if number * number > max then numberSequence else
        let filteredNumbers = numberSequence |> Seq.filter (fun v -> v = number || v % number <> 0L)
        let newNumberSequence = seq { for i in filteredNumbers -> i }
        let newNumber = newNumberSequence |> Seq.find (fun x -> x > number)
        generate newNumber newNumberSequence                
    generate 2L (seq { for i in 2L..max -> i })

Обновление

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

/// 5.2s for max = 2,000,000
let generatePrimeNumbers max =    
    let rec generate number numberSequence =
        if number * number > max then numberSequence else
        let filteredNumbers = numberSequence |> Seq.filter (fun v -> v = number || v % number <> 0L) |> Seq.toArray |> Array.toSeq
        let newNumber = filteredNumbers |> Seq.find (fun v -> v > number)
        generate newNumber filteredNumbers                
    generate 2L (seq { for i in 2L..max -> i })

Обновление

Видимо, я использовал старый компилятор. В последней версии мой оригинальный алгоритм занимает 6,5 с, а не 8 с. Это значительное улучшение.

Ответы [ 5 ]

8 голосов
/ 13 января 2010

Функция Томаса Петричека довольно быстрая, но мы можем сделать это немного быстрее.

Сравните следующее:

let is_prime_tomas n =
    let ms = int64(sqrt(float(n)))
    let rec isPrimeUtil(m) =
        if m > ms then true
        elif n % m = 0L then false
        else isPrimeUtil(m + 1L)
    (n > 1L) && isPrimeUtil(2L)

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

is_prime_juliet имеет немного более быстрый внутренний цикл. Это хорошо известная стратегия генерации простых чисел, которая использует «переключение» для пропуска не простых чисел с шагом 2 или 4. Для сравнения:

> seq { 2L .. 2000000L } |> Seq.filter is_prime_tomas |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:03.628, CPU: 00:00:03.588, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933

> seq { 2L .. 2000000L } |> Seq.filter is_prime_juliet |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:01.530, CPU: 00:00:01.513, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933

Моя версия примерно в 2,37 раза быстрее, и она также довольно близка к скорости самых быстрых императивных версий. Мы можем сделать это еще быстрее , потому что нам не нужно фильтровать список 2L .. 2000000L, мы можем использовать ту же стратегию для генерации более оптимальной последовательности возможных простых чисел, прежде чем применять наш фильтр:

> let getPrimes upTo =
    seq {
        yield 2L;
        yield 3L;
        yield 5L;
        yield! (7L, 4L) |> Seq.unfold (fun (p, tog) -> if p <= upTo then Some(p, (p + tog, 6L - tog)) else None)
    }
    |> Seq.filter is_prime_juliet;;
Real: 00:00:00.000, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0

val getPrimes : int64 -> seq<int64>

> getPrimes 2000000L |> Seq.fold (fun acc _ -> acc + 1) 0;;
Real: 00:00:01.364, CPU: 00:00:01.357, GC gen0: 36, gen1: 1, gen2: 0
val it : int = 148933

Мы упали с 1,530 до 01,364 с, поэтому мы увеличили скорость примерно на 1,21 раза. Удивительный!

7 голосов
/ 13 января 2010

Только для забавы, давайте посмотрим на эту страницу .

pi (x) - функция подсчета простых чисел, она возвращает число простых чисел ниже x. Вы можете приблизить число пи (х), используя следующие неравенства:

(x/log x)(1 + 0.992/log x) < pi(x) < (x/log x)(1 + 1.2762/log x) 
// The upper bound holds for all x > 1

p (x) - первичная функция n-го числа, которую можно аппроксимировать с помощью:

n ln n + n ln ln n - n < p(n) < n ln n + n ln ln n
// for n >= 6

Имея это в виду, - это очень быстрый генератор , который вычисляет первые n простых чисел, где каждый элемент с индексом i равен p(i). Итак, если мы хотим ограничить наш массив простыми числами ниже 2 000 000, мы используем верхнее неравенство для функции простого подсчета:

let rec is_prime (primes : int[]) i testPrime maxFactor =
    if primes.[i] > maxFactor then true
    else
        if testPrime % primes.[i] = 0 then false
        else is_prime primes (i + 1) testPrime maxFactor

let rec prime_n (primes : int[]) primeCount testPrime tog =
    if primeCount < primes.Length then
        let primeCount' =
            if is_prime primes 2 testPrime (float testPrime |> sqrt |> int) then
                primes.[primeCount] <- testPrime
                primeCount + 1
            else
                primeCount
        prime_n primes primeCount' (testPrime + tog) (6 - tog)

let getPrimes upTo =
    let x = float upTo
    let arraySize = int <| (x / log x) * (1.0 + 1.2762 / log x)
    let primes = Array.zeroCreate (max arraySize 3)
    primes.[0] <- 2
    primes.[1] <- 3
    primes.[2] <- 5

    prime_n primes 3 7 4
    primes

Cool! Так как быстро это? На моем 3,2 ГГц четырехъядерном я получаю следующее в fsi:

> let primes = getPrimes 2000000;;
Real: 00:00:00.534, CPU: 00:00:00.546, GC gen0: 0, gen1: 0, gen2: 0

val primes : int [] =
  [|2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59; 61; 67; 71;
    73; 79; 83; 89; 97; 101; 103; 107; 109; 113; 127; 131; 137; 139; 149; 151;
    157; 163; 167; 173; 179; 181; 191; 193; 197; 199; 211; 223; 227; 229; 233;
    239; 241; 251; 257; 263; 269; 271; 277; 281; 283; 293; 307; 311; 313; 317;
    331; 337; 347; 349; 353; 359; 367; 373; 379; 383; 389; 397; 401; 409; 419;
    421; 431; 433; 439; 443; 449; 457; 461; 463; 467; 479; 487; 491; 499; 503;
    509; 521; 523; 541; ...|]

> printfn "total primes: %i. Last prime: %i" (primes.Length - 1) primes.[primes.Length - 1];;
total primes: 149973. Last prime: 2014853

Итак, все простые числа около 2 миллионов менее чем за полсекунды:)

4 голосов
/ 13 января 2010

РЕДАКТИРОВАТЬ: обновленная версия ниже, использует меньше памяти и быстрее

Иногда хорошо иметь возможность мутировать вещи. Вот, по общему признанию, весьма императивная версия, которая обменивает память на скорость. Поскольку в этом потоке оказалась замечательная коллекция простых генерирующих функций в F #, я подумал, что было бы неплохо добавить мою в любом случае. Использование BitArray позволяет контролировать переполнение памяти.

open System.Collections

let getPrimes nmax =
    let sieve = new BitArray(nmax+1, true)
    let result = new ResizeArray<int>(nmax/10)
    let upper = int (sqrt (float nmax))   
    result.Add(2)

    let mutable n = 3
    while n <= nmax do
       if sieve.[n] then
           if n<=upper then 
               let mutable i = n
               while i <= nmax do sieve.[i] <- false; i <- i + n
           result.Add n
       n <- n + 2
    result

Обновление:

Приведенный выше код может быть дополнительно оптимизирован: поскольку он использует только нечетные индексы в сите, BitArray можно уменьшить до половины размера, индексируя нечетное n как 2m + 1. Новая версия также быстрее:

let getPrimes2 nmax =
    let sieve = new BitArray((nmax/2)+1, true)
    let result = new ResizeArray<int>(nmax/10)
    let upper = int (sqrt (float nmax))   
    if nmax>1 then result.Add(2) //fixes a subtle bug for nmax < 2

    let mutable m = 1
    while 2*m+1 <= nmax do
       if sieve.[m] then
           let n = 2*m+1
           if n <= upper then 
               let mutable i = m
               while 2*i < nmax do sieve.[i] <- false; i <- i + n
           result.Add n
       m <- m + 1
    result

Время (Intel Core Duo 2,33 ГГц):

> getPrimes 2000000 |> Seq.length;;
Real: 00:00:00.037, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933
> getPrimes2 2000000 |> Seq.length;;
Real: 00:00:00.026, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
val it : int = 148933
3 голосов
/ 13 января 2010

Императивная версия, опубликованная Инь, очень быстрая. На моей машине это тоже около 0.5сек. Однако, если вы хотите написать простое функциональное решение, вы можете просто написать это:

let isPrime(n) =
  let ms = int64(sqrt(float(n)))
  let rec isPrimeUtil(m) =
    if m > ms then true
    elif n % m = 0L then false
    else isPrimeUtil(m + 1L)
  (n > 1L) && isPrimeUtil(2L)

[ 1L .. 2000000L ] |> List.filter isPrime

Это просто проверяет, является ли число простым числом для всех чисел до 1 миллиона. Он не использует никаких сложных алгоритмов (на самом деле забавно, что решение, которое является самым простым, часто достаточно хорошо!). На моем компьютере ваша обновленная версия работает около 11 секунд, и это работает около 2 секунд.

Что еще интереснее, это очень легко распараллелить. Если вы используете PLINQ, вы можете написать версию ниже, и она будет работать почти в 2 раза быстрее на двухъядерном. Это означает, что в четырехъядерном процессоре это может быть так же быстро, как самое быстрое решение из всех приведенных здесь ответов, но с минимальными затратами на программирование :-) (конечно, использование четырех ядер не экологично, но .. хорошо)

[ 1L .. 2000000L ] |> PSeq.ofSeq |> PSeq.filter isPrime |> List.ofSeq

Функции PSeq являются обертками для PLINQ, которые я создал для своей книги (это делает использование PLINQ из F # более естественным). Они доступны в исходном коде для главы 14 .

2 голосов
/ 13 января 2010

Я написал императивную версию, которая быстрее. Может быть невозможно написать Sieve of Eratosthenes чисто функциональным способом для достижения той же скорости, так как для каждого числа необходимо иметь двоичное состояние.

let generatePrimes max=
    let p = Array.create (max+1) true
    let rec filter i step = 
        if i <= max then 
            p.[i] <- false
            filter (i+step) step
    {2..int (sqrt (float max))} |> Seq.map (fun i->filter (i+i) i) |> Seq.length |> ignore
    {2..max} |> Seq.filter (fun i->p.[i]) |> Seq.toArray
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...