My question is about the conditional test in trial division. There seems to be some debate on what conditional test to employ. Let's look at the code for this from RosettaCode.
int is_prime(unsigned int n)
{
unsigned int p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p <= n/p; p += 2)
if (!(n % p)) return 0;
return 1;
}
Wheel factorization or using a predetermined list of primes will not change the essence of my question.
There are three cases I can think of to do the conditional test:
- p<=n/p
- p*p<=n
- int cut = sqrt(n); for (p = 3; p <= cut; p += 2)
Case 1: works for all n but it has to do an extra division every iteration (edit: actually it does not need an extra division but it's still slower. I'm not sure why. See the assembly output below). I have found it to be twice as slow as case 2 for large values of n which are prime (on my Sandy Bridge system).
Case 2: is signficantly faster than case 1 but it has a problem that it overflows for large n and goes into an infinitive loop. The max value it can handle is
(sqrt(n) + c)^2 = INT_MAX //solve
n = INT_MAX -2*c*sqrt(INT_MAX) + c^2
//INT_MAX = 2^32 -> n = 2^32 - c*s^17 + c^2; in our case c = 2
For example for uint64_t case 2 will go into an infinite loop for x =-1L-58 (x^64-59) which is a prime.
Case 3: no division or multiplication has to be done every iteration and it does not overflow like case 2. It's slightly faster than case 2 as well. The only question is if sqrt(n) is accurate enough.
Can someone explain to me why case 2 is so much faster than case 1? Case 1 does NOT use an extra division as I though but despite that it's still a lot slower.
Here are the times for the prime 2^56-5;
case 1 9.0s
case 2 4.6s
case 3 4.5s
Here is the code I used to test this http://coliru.stacked-crooked.com/a/69497863a97d8953. I also added the functions to the end of this question.
Here is the assembly output form GCC 4.8 with -O3 for case 1 and case 2. They both only have one division. Case 2 has a multiplication as well so my first guess is that case 2 would be slower but it's about twice as fast on both GCC and MSVC. I don't know why.
Case 1:
.L5:
testl %edx, %edx
je .L8
.L4:
addl $2, %ecx
xorl %edx, %edx
movl %edi, %eax
divl %ecx
cmpl %ecx, %eax
jae .L5
Case 2:
.L20:
xorl %edx, %edx
movl %edi, %eax
divl %ecx
testl %edx, %edx
je .L23
.L19:
addl $2, %ecx
movl %ecx, %eax
imull %ecx, %eax
cmpl %eax, %edi
jae .L20
Here are the functions I'm using to test the time:
int is_prime(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p <= n/p; p += 2)
if (!(n % p)) return 0;
return 1;
}
int is_prime2(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
for (p = 3; p*p <= n; p += 2)
if (!(n % p)) return 0;
return 1;
}
int is_prime3(uint64_t n)
{
uint64_t p;
if (!(n & 1) || n < 2 ) return n == 2;
/* comparing p*p <= n can overflow */
uint32_t cut = sqrt(n);
for (p = 3; p <= cut; p += 2)
if (!(n % p)) return 0;
return 1;
}
Added content after the bounty.
Aean discovered that in case 1 saving the quotient as well as the remainder is just as fast (or slightly faster) than case 2. Let's call this case 4. The following following code is twice as fast as case 1.
int is_prime4(uint64_t n)
{
uint64_t p, q, r;
if (!(n & 1) || n < 2 ) return n == 2;
for (p = 3, q=n/p, r=n%p; p <= q; p += 2, q = n/p, r=n%p)
if (!r) return 0;
return 1;
}
I'm not sure why this helps. In any case there is no need to use case 2 anymore. For case 3, most versions of the sqrt
function in hardware or software get the perfect squares right so it's safe to use in general. Case 3 is the only case that will work with OpenMP.
See Question&Answers more detail:
os