How do I efficiently generate a list of primes in Perl 6?

If you run your program with --profile, you will see that more than 99% of the time is spent in Int.is-prime. Since that effectively is just a wrapper around nqp::isprime_I(), I have tried to run similar code without the wrapper. But that doesn't change anything noticeably. So the brunt of the work is being done in nqp::isprime_I().

So the only option you really have is to parallelize your searches for primes. In the (nearer) future, hyper would be your friend here. But that is currently at the "initial naive implementation" phase, with a more robust implementation being discussed in: https://gist.github.com/jnthn/6a80a9712fb38b32537f9f0e46fca6d7

Until then, if you want to run things faster, you would have to manually break up the range of values you want to check for primedness and run them within a start block, and collect the results from the resulting Promise.


I wrote some Perl 6 bindings for primesieve:

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

Math::Primesieve


This isn't really an answer to my question, but I benchmarked a few ways to get a list of primes.

Conclusions:

  1. .is-prime is indeed way too slow for this (although @DanaJ's branch hopefully improves this a bit).
  2. A sieve in Perl 6 code isn't as slow as I feared, as long as you optimize things a bit (i.e. make the code less pretty Perl6ish).
  3. Native code (by way of a Perl 5 module) is still way faster.
  4. Cheating is the fastest.

Edit: added Curt Tilmes' Primesieve module. Wow, it's fast! It beats cheating (i.e. reading primes from a file)! Well, that might be because Primesieve doesn't support a generator/iterator (yet?), so I'm just returning the whole list at once, while all other versions use a generator / lazy list.

Edit: added “even more optimized” sieve based on Timbus' comment. This one has a pretty decent performance, but the code is almost impossible to follow...

Edit: added an even better pure Perl6 version with a flexible sieve size. I start with a (pre-initialized) sieve for the odd numbers 1..99, and double the sieve size as necessary. Together with some further optimizations (e.g. when extending the sieve, we only have to check primes up to √(sieve size)) this is by far the fastest pure Perl 6 version so far. Further advantages: you don't have to know a limit in advance; and it gives the small primes a lot faster if you do need a high limit.

Edit: Math::Primesieve now supports an iterator, so include that in the script.

#!/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

Tags:

Primes

Raku