Как эффективно создать список простых чисел в Perl 6?

Создать список простых чисел в Perl 6 невероятно просто, используя is-prime:

my @primes = (^∞).grep: *.is-prime;

Это работает достаточно хорошо, если вам нужно относительно небольшое количество простых чисел, но очень неэффективно для больших чисел, поскольку каждое число проверяется независимо.

Есть ли способ получить доступ к встроенной в Perl 6 логике проверки простых чисел для эффективного создания списка простых чисел? В противном случае мне придется самому построить сито. Достаточно просто, но я боюсь, что сито в высокоуровневом коде Perl 6 будет почти таким же неэффективным, как код, с которого я начал.


person mscha    schedule 29.04.2017    source источник
comment
Стоит отметить, что процедура is-prime в Perl 6 является вероятностной.   -  person    schedule 30.04.2017
comment
Дох, нужно проверить ветку с гораздо более быстрым исходным числом (это также детерминировано для 64-битных входных данных). Вы могли бы также написать Решето Эратосфена.   -  person DanaJ    schedule 01.05.2017
comment
@DanaJ: да, ты обещал нам это довольно давно :-) Никакого давления!   -  person Elizabeth Mattijsen    schedule 01.05.2017
comment
@Zoffix: хорошая мысль; поэтому моя идея «заимствования» кода для более эффективного получения списка простых чисел на самом деле не имеет никакого смысла.   -  person mscha    schedule 02.05.2017
comment
@DanaJ: намного быстрее, это звучит потрясающе. Жаль, что он пропустил недавний выпуск Rakudo, теперь его не будет в Rakudo Star до 2014.07. ????   -  person mscha    schedule 02.05.2017


Ответы (3)


Я написал несколько привязок Perl 6 для primesieve:

https://github.com/CurtTilmes/perl6-primesieve

Математика::Примеси

person Curt Tilmes    schedule 02.05.2017
comment
Хороший! Я добавил его в свои тесты, и он уничтожает все остальные версии, включая читерство (= чтение простых чисел из текстового файла)! Это может быть частично связано с тем, что я просто возвращаю весь список простых чисел, а не использую ленивый список, как в других версиях. Запрос на улучшение: было бы здорово, если бы вы также могли раскрыть primesieve::iterator. - person mscha; 02.05.2017
comment
libprimesieve по умолчанию включает итератор, и я начал добавлять его, но основная функция определена в заголовочном файле как «статическая встроенная» функция. Это делает его быстрым, но символ не попадает в библиотеку, и я не могу вызвать его напрямую с помощью Nativecall. Мне пришлось бы создать пользовательскую версию библиотеки, которая компилирует эту функцию как вызываемый библиотечный символ. - person Curt Tilmes; 02.05.2017
comment
Это очень плохо. Тем не менее, если вы заранее знаете, сколько простых чисел вам нужно (или, по крайней мере, имеете разумный верхний предел), Math::Primesieve непобедима! - person mscha; 02.05.2017
comment
Превзойти файловый ввод-вывод не так уж сложно, но Primesieve выходит за рамки этого. Это самый быстрый код C с большим отрывом, за исключением внутреннего сита yafu (что довольно близко, но также не может использоваться вне этой программы). Спасибо за привязки Perl6! - person DanaJ; 04.05.2017
comment
Я вижу, что Math::Primesieve теперь указан на modules.perl6.org и может быть просто установлен с помощью zef install Math::Primesieve. Хороший! Спасибо, @Керт! - person mscha; 04.05.2017
comment
@mscha -- Попробуйте прямо сейчас. Я думаю, что у меня работает итератор. - person Curt Tilmes; 13.05.2017
comment
@курт - Аккуратно! К сожалению, итератор сбрасывает ядро ​​на моей машине. См. github.com/CurtTilmes/perl6-primesieve/issues/3 . - person mscha; 13.05.2017
comment
И дамп ядра был исправлен! Math::Primesieve::iterator добавлен в скрипт в моем ответе на тест. - person mscha; 21.05.2017

Если вы запустите свою программу с --profile, вы увидите, что более 99% времени тратится на Int.is-prime. Поскольку это фактически просто оболочка вокруг nqp::isprime_I(), я попытался запустить аналогичный код без оболочки. Но это ничего заметно не меняет. Таким образом, основная работа выполняется в nqp::isprime_I().

Таким образом, единственный вариант, который у вас действительно есть, — это распараллелить поиск простых чисел. В (ближайшем) будущем hyper будет вашим другом здесь. Но в настоящее время это находится на этапе «начальной наивной реализации», а более надежная реализация обсуждается в: /gist.github.com/jnthn/6a80a9712fb38b32537f9f0e46fca6d7

До тех пор, если вы хотите работать быстрее, вам придется вручную разбить диапазон значений, которые вы хотите проверить на первичность, и запустить их в блоке start, а затем собрать результаты из полученного блока Promise.

person Elizabeth Mattijsen    schedule 30.04.2017
comment
Спасибо за советы. Однако это было не совсем то, что я искал; Я хотел использовать внутренний код, чтобы предотвратить проверку один за другим. Но я узнал от @Zoffix, что это невозможно, поскольку внутренний код выполняет не сито или что-то подобное, а тест на простоту. Это всегда будет слишком неэффективно для создания списка простых чисел. - person mscha; 02.05.2017

На самом деле это не ответ на мой вопрос, но я проверил несколько способов получить список простых чисел.

Выводы:

  1. .is-prime действительно слишком медленный для этого (хотя ветка @DanaJ, надеюсь, немного улучшит это).
  2. Сито в коде Perl 6 не такое медленное, как я опасался, если вы немного оптимизируете вещи (т. е. делаете код менее привлекательным Perl6).
  3. Нативный код (с помощью модуля Perl 5) по-прежнему намного быстрее.
  4. Обман самый быстрый. ????

Изменить: добавлен модуль Primesieve Курта Тилмеса. Вау, это быстро! Это лучше, чем читерство (т.е. чтение простых чисел из файла)! Ну, это может быть потому, что Primesieve не поддерживает генератор/итератор (пока?), поэтому я просто возвращаю весь список сразу, в то время как все другие версии используют генератор/ленивый список.

Редактировать: добавлено «еще более оптимизированное» сито на основе комментария Тимбуса. У этого довольно приличная производительность, но код почти невозможно следовать...

Редактировать: добавлена ​​еще лучшая версия Perl6 с гибким размером сетки. Я начинаю с (предварительно инициализированного) сита для нечетных чисел 1..99 и при необходимости удваиваю размер сита. Вместе с некоторыми дополнительными оптимизациями (например, при расширении сита нам нужно проверять только простые числа до √(размер сита)) это, безусловно, самая быстрая чистая версия Perl 6 на данный момент. Дополнительные преимущества: вам не нужно заранее знать лимит; и это дает маленькие простые числа намного быстрее, если вам нужен высокий предел.

Изменить: Math::Primesieve теперь поддерживает итератор, поэтому включите его в сценарий.

#!/usr/bin/env perl6

use v6.c;

# The easy but slow way
sub primes-is-prime
{
    (^∞).grep: *.is-prime;
}

# Use a sieve (simple Perl 6 style)
sub primes-sieve(Int $max)
{
    my @sieve;
    lazy gather for 2..$max -> $p {
        next if @sieve[$p];
        take $p;
        for 2*$p, 3*$p ... $max -> $n {
            @sieve[$n] = True;
        }
    }
}

# Use a sieve (optimized)
sub primes-sieve2(Int $max)
{
    my int @sieve;
    lazy gather {
        take 2;
        loop (my int $p = 3; $p ≤ $max; $p += 2) {
            next if @sieve[$p];
            take $p;
            loop (my int $n = 3*$p; $n ≤ $max; $n += 2*$p) {
                @sieve[$n] = 1;
            }
        }
    }
}

# Use a sieve (even more optimized)
sub primes-sieve3(Int $max)
{
    my int @sieve;
    my $max2 = ($max-1) div 2;
    lazy gather {
        take 2;
        for 1 .. $max2 -> int $i {
            next if @sieve[$i];
            take 2*$i + 1;
            my $max3 = ($max2 - $i) div (2*$i + 1);
            for 1 .. $max3 -> int $j {
                @sieve[(2*$j + 1)*$i + $j] = 1;
            }
        }
    }
}

# Use a flexible sieve size (and further optimized)
sub primes-sieve4
{
    # Pre-initialize our sieve with the odd numbers from 1 to 99
    my $max = 100;
    my int @sieve = 1,0,0,0,1,0,0,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,
                    1,0,1,1,0,0,1,1,0,1,0,0,1,1,0,1,0,1,1,0,1,1,1,0,1;

    lazy gather {
        # Don't forget our single even prime number
        take 2;

        my int $min-i = 1;
        loop {
            # Take all primes in the new part of the sieve 
            my int $max-i = ($max-1) div 2;
            for $min-i .. $max-i -> int $i {
                take 2*$i + 1 unless @sieve[$i];
            }

            # Extend sieve by factor 2
            # We must check the primes from 3 to √(2*max) in the sieve
            # for max to 2*max
            for 1 .. ((2*$max).sqrt.floor-1) div 2 -> int $i {
                next if @sieve[$i];
                my int $p = 2*$i + 1;
                my int $min-j = max(($max-i - $i) div $p, $i);
                my int $max-j = (2*$max-i + 1 - $i) div $p;
                for $min-j .. $max-j -> int $j {
                    @sieve[$i + $p*$j] = 1;
                }
            }

            # Double the sieve size, and start the next iteration
            # in the second half of the sieve
            $max *= 2;
            $min-i = $max-i+1;
        }
    }
}

# Use a Perl 5 module
sub primes-perl5
{
    use Math::Prime::Util:from<Perl5> <prime_iterator>;

    my $it = prime_iterator;
    lazy gather {
        loop {
            take $it.();
        }
    }
}

# Use Primesieve module
sub primes-primesieve($max)
{
    # See:
    #  - http://primesieve.org/
    #  - https://github.com/CurtTilmes/perl6-primesieve
    use Math::Primesieve;

    # No iterator support (yet?), so just return the whole list
    return Math::Primesieve.new.primes($max);
}

# Use Primesieve module (iterator)
sub primes-primesieve-iterator
{
    # See:
    #  - http://primesieve.org/
    #  - https://github.com/CurtTilmes/perl6-primesieve
    use Math::Primesieve;
    my $iterator = Math::Primesieve::iterator.new;
    lazy gather {
        loop {
            take $iterator.next;
        }
    }
}

# Cheat
# Source: https://primes.utm.edu/lists/small/millions/ - first million
# (Unzip and remove the first few lines from the file.)
sub primes-cheat
{
    lazy $*PROGRAM.sibling('primes1.txt').words.map(+*);
}

sub timer(&code)
{
    my $start = now;
    &code();
    my $elapsed = now - $start;
    say "Elapsed: $elapsed.fmt('%.3f')s";
}

sub MAIN
{
    #my $nth = 1_000;
    #my $max = 8_000;

    #my $nth = 10_000;
    #my $max = 105_000;

    my $nth = 50_000;
    my $max = 612_000;

    timer {
        my @primes = primes-is-prime;
        say "Using .is-prime: @primes[$nth]";
    }
    timer {
        my @primes = primes-sieve($max);
        say "Using a sieve (simple Perl 6 style): @primes[$nth]";
    }
    timer {
        my @primes = primes-sieve2($max);
        say "Using a sieve (optimized): @primes[$nth]";
    }
    timer {
        my @primes = primes-sieve3($max);
        say "Using a sieve (even more optimized): @primes[$nth]";
    }
    timer {
        my @primes = primes-sieve4;
        say "Using a flexible sieve size (further optimized): @primes[$nth]";
    }
    timer {
        my @primes = primes-perl5;
        say "Using a Perl 5 module: @primes[$nth]";
    }
    timer {
        my @primes = primes-primesieve($max);
        say "Using Primesieve module: @primes[$nth]";
    }
    timer {
        my @primes = primes-primesieve-iterator;
        say "Using Primesieve module (iterator): @primes[$nth]";
    }
    timer {
        my @primes = primes-cheat;
        say "Cheating: @primes[$nth]";
    }
}

# 4 year old Linux server, running Rakudo Star 2017.04:
#
# Using .is-prime: 611957
# Elapsed: 216.134s
# Using a sieve (simple Perl 6 style): 611957
# Elapsed: 124.087s
# Using a sieve (optimized): 611957
# Elapsed: 41.129s
# Using a sieve (even more optimized): 611957
# Elapsed: 7.285s
# Using a flexible sieve size (further optimized): 611957
# Elapsed: 3.897s
# Using a Perl 5 module: 611957
# Elapsed: 10.031s
# Using Primesieve module: 611957
# Elapsed: 0.312s
# Using Primesieve module (iterator): 611957
# Elapsed: 1.460s
# Cheating: 611957
# Elapsed: 2.017s
person mscha    schedule 01.05.2017
comment
Интересно .. Дополнительное примечание: замена обоих primes-sieve2 loop на for 2..$max/2 -> $x { my $p = $x*2+1; ... } (то же эффективное количество циклов) дает ощутимое увеличение производительности (с 13,0 до 7,8 для меня). Выглядит еще более уродливо, но вот. - person Jarrod Funnell; 02.05.2017
comment
Хорошо, @Timbus. Когда я попробовал, это немного спасло, но не так сильно, как для вас. Но когда я проделал то же самое с внутренним циклом, это сэкономило много времени! (Я также оптимизировал использование сита: используйте его только для нечетных чисел, поэтому оно вдвое меньше.) - person mscha; 02.05.2017
comment
И я только что понял, что это то же самое, что и вы («замена обоих циклов Primes-sieve2»). - person mscha; 02.05.2017
comment
Да, но вы сделали это быстрее, уменьшив массив вдвое. Также здесь попытка сделать код более разборчивым: gist.github.com/TiMBuS/2965b4f3c5ff29c165b56a13f2fbc668 - person Jarrod Funnell; 03.05.2017