3

I am looking for a prime-sieve implementation which is efficient in terms of memory consumption.

Of course, the primality-test itself should execute at a constant and minimal number of operations.

I have implemented a sieve that indicates primality only for numbers that are adjacent to multiples of 6.

For any other number, either it is 2 or 3 (therefore prime), or it is a multiple of 2 or 3 (therefore non-prime).

So this is what I came up with, and I've been wondering if there is anything better under those requirements:

Interface:

#include <limits.h>

// Defined by the user (must be less than 'UINT_MAX')
#define RANGE 4000000000

// The actual length required for the prime-sieve array
#define ARR_LEN (((RANGE-1)/(3*CHAR_BIT)+1))

// Assumes that all entries in 'sieve' are initialized to zero
void Init(char sieve[ARR_LEN]);

// Assumes that 'Init(sieve)' has been called and that '1 < n < RANGE'
int IsPrime(char sieve[ARR_LEN],unsigned int n);

#if RANGE >= UINT_MAX
    #error RANGE exceeds the limit
#endif

Implementation:

#include <math.h>

#define GET_BIT(sieve,n) ((sieve[(n)/(3*CHAR_BIT)]>>((n)%(3*CHAR_BIT)/3))&1)
#define SET_BIT(sieve,n) sieve[(n)/(3*CHAR_BIT)] |= 1<<((n)%(3*CHAR_BIT)/3)

static void InitOne(char sieve[ARR_LEN],int d)
{
    unsigned int i,j;
    unsigned int root = (unsigned int)sqrt((double)RANGE);

    for (i=6+d; i<=root; i+=6)
    {
        if (GET_BIT(sieve,i) == 0)
        {
            for (j=6*i; j<RANGE; j+=6*i)
            {
                SET_BIT(sieve,j-i);
                SET_BIT(sieve,j+i);
            }
        }
    }
}

void Init(char sieve[ARR_LEN])
{
    InitOne(sieve,-1);
    InitOne(sieve,+1);
}

int IsPrime(char sieve[ARR_LEN],unsigned int n)
{
    return n == 2 || n == 3 || (n%2 != 0 && n%3 != 0 && GET_BIT(sieve,n) == 0);
}
Will Ness
  • 70,110
  • 9
  • 98
  • 181
barak manos
  • 29,648
  • 10
  • 62
  • 114
  • This question appears to be off-topic because it does not have a specific programming question. – abelenky Jan 08 '15 at 22:37
  • Someone with a rep. of 17,000 should really recognize that StackOverflow is about specific programming problems that have specific answers. Questions such as "is there anything better" do not fit well with the Q&A format here. – abelenky Jan 08 '15 at 22:38
  • @abelenky: Yeah I got your point in the first comment, but I am asking for a prime-sieve optimized for memory. If I hadn't shown any effort, then... well, you know what kind of comments I would have gotten. So I have shared my thoughts, and now I am asking for improvements. It can be an algorithmic solution, **OR** - if somebody spots something obvious in my implementation, then it can also be a suggestion for improvement. In either case, the programming question should be very clear to someone with whatever your reputation is (pardon me for not checking it). – barak manos Jan 08 '15 at 22:42
  • 1
    I'm sure you're familiar with [CodeReview](http://codereview.stackexchange.com/). The first "comment" is not a deliberate comment. Its just a side-effect of voting to close. – abelenky Jan 08 '15 at 22:44
  • @barakmanos The macros index the array with `[n / (3*CHAR_BIT)]`, but the array size is declared as `RANGE/3`. Shouldn't that be `RANGE/24`? – user3386109 Jan 08 '15 at 22:46
  • @abelenky: As you can see, somebody already came up with an algorithmic suggestion. I did take CodeReview into consideration before posting this question, but it is not a "clear cut" coding-review question, but more of a mixture of code-review and programming/algorithmic advices. So I believe that it is appropriate enough for this site. – barak manos Jan 08 '15 at 22:49
  • @user3386109: Yep, that's definitely a mistake that have cost me 8 times the amount of memory required. Fixed it in the question. Thanks. – barak manos Jan 08 '15 at 22:51
  • @barakmanos I think they say somewhere that if the code works, its place is in the CodeReview, period. the 3/24 advice is exactly the kind you'd get *there* (except for the lower traffic...). about your question, check out the "segmented sieve", with segment size such that it fits in your cache memory. – Will Ness Jan 09 '15 at 10:15
  • @WillNess: The "3/24 advice" was given in a comment. The answer below by user3386109 gives an alternative solution, which is what I was looking for. By the way, the "3/24" solution is already applied in my my code, so that advice was in fact more of a correction. And while it is true that the "advice" could have been given as an answer on CodeReview, the actual answer below fits right into the purpose of my question. – barak manos Jan 09 '15 at 10:52
  • @WillNess: As I've already mentioned to the other guy (or girl) who implied that the question was not eligible for StackOverflow - it is not a "clear cut" coding-review question, but more of a mixture of code-review and programming/algorithmic advices. So I believe that it is appropriate enough for this site. – barak manos Jan 09 '15 at 10:52

2 Answers2

3

You've correctly deduced that you can exploit the fact that there are only two number that are relatively prime to 6, i.e. 1 and 5 (aka +1 and -1). Using that fact and storing the sieve as bits instead of bytes, you reduce the memory requirement by a factor of 24.

To save a little more memory, you can go to the next level and note that there are only 8 numbers (modulo 30) that are relatively prime to 30. They are 1, 7, 11, 13, 17, 19, 23, 29. Using that fact and storing as bits, the memory is reduced by a factor of 30.

Implementation note: each byte in the sieve represents the 8 numbers relatively prime to some multiple of 30. For example, the bits contained in sieve[3] represent the numbers 91, 97, 101, ...

user3386109
  • 34,287
  • 7
  • 49
  • 68
  • OK, it took me some time to understand this answer, because the statement "there are only 8 numbers that are relatively prime to 30" isn't quite correct (for example - 31, 37, etc). What you meant is, that in each section of 30 numbers, there are **at most** 8 primes, and that in addition, I will need to check divisibility with 2, 3 and 5... right? – barak manos Jan 08 '15 at 23:05
  • @barakmanos Yes, that's correct. What I suggest is a lookup table with 30 entries, where each entry in the table is either 0 (for those numbers that are multiples of 2,3, or 5), or a bit mask for the 8 numbers that may be primes. So given a number `N` you compute `N % 30`. If the corresponding entry in the table is 0, then `N` is a multiple of 2,3, or 5. Otherwise, you have a bit mask to use with `sieve[N/30]`. – user3386109 Jan 08 '15 at 23:41
  • Oh, well that's certainly different from my original approach. There are no entries in my array for those numbers that are multiples of 2 or 3 (I check them without using the sieve). – barak manos Jan 08 '15 at 23:43
  • @barakmanos Yes, in your case it's easier to map numbers to bits, since there are only two numbers in each section of 6 numbers. With 8 oddball numbers in each section of 30 numbers, there's no easy formula to map numbers to bits. – user3386109 Jan 08 '15 at 23:46
  • OK, so I need to have another look at your solution and try to figure it out then. – barak manos Jan 08 '15 at 23:48
  • @barakmanos The mod-30 wheel is fairly common for a good memory/complexity tradeoff. I've seen 210 and 2310 but it's a lot of work for very small gains. Once your sizes get large, good primality tests would be better for isprime (e.g. BPSW or deterministic M-R), and will be deterministic and fast for 64 bits without any sieving. While not seeming to be the OP application, the OP first sentence brings to mind segmented sieves, which allow bounded memory (obviously they don't *store* them all). – DanaJ Jan 10 '15 at 07:38
1

For numbers less than the max bit size you can use a single "primes" mask with a 1 in each prime position.

if (n < 64) return !!(primes & 1ull<<n);

To filter out multiples of 2, 3 & 5 for higher values of n you can use n%30 along with a notprime mask (note that the product of the first 3 primes is a constant of 30, so most compilers will optimize it to multiply and shift)

else if (notprime & 1ull << n%30) return 0;

For the remaining values that may be prime, you can do a manual check.

else return isprime(n);

Since each block of 30 numbers contains 8 possible primes IIRC, the isprime function can be vectorized by simply adding 30 to each successive block or you can trade size for speed and store precalculated multiply and shift values in a constant vector array to avoid expensive divisions. Alternatively you can use 8bit masks to store which of the maybe-primes are prime as you go, so that you only need to compute it once.

technosaurus
  • 7,676
  • 1
  • 30
  • 52