0

I've written a small C program to calculate primes and now try to optimize the code as far as I can.

In the first revision of the program I was checking if a number was even (modulo 2) and if it was I would continue to the next number.

In my second revision I tried only checking odd numbers to be possible primes by incrementing the number I would be checking by 2 (so I would start from 3, then check 5, then 7, then 9, then 11 etc etc).

I thought this would be way faster as I had cut an extra check for modulo 2 with my code and simply replaced it with an addition. However, to my surprise the code checking only odd numbers, runs a tad bit slower most of the times then the implementation checking all numbers.

Here is the code(it contains the changes I made between revisions in comments wherever it says //CHANGE)

#include <stdio.h>
#include <stdbool.h>
#include <math.h>

unsigned long i = 3; //CHANGE No 1 - it was i = 2;

bool hasDivisor(unsigned long number)
{
    //https://stackoverflow.com/questions/5811151/why-do-we-check-up-to-the-square-root-of-a-prime-number-to-determine-if-it-is-pr
    unsigned long squareRoot = floor(sqrt(number));

    //we don't check for even divisors - we want to check only odd divisors
    //CHANGE No 2 - it was (number%2 ==0)
    if(!((squareRoot&1)==0)) //thought this would boost the speed
    {  
        squareRoot += 1;
    }

    for(unsigned long j=3; j <= squareRoot; j+=2)
    {
        //printf("checking number %ld with %ld \n", number, j);
        if(number%j==0)
        {
            return true;
        }
    }
    return false;
}

int main(int argc, char** argv)
{
    printf("Number 2 is a prime!\n");
    printf("Number 3 is a prime!\n");

    while(true)
    {
        //even numbers can't be primes as they are a multiple of 2
        //so we start with 3 which is odd and contiously add 2
        //that we always check an odd number for primality
        i++; //thought this would boost the speed instead of i +=2;
        i++; //CHANGE No 3 - it was a simple i++ so eg 3 would become 4 and we needed an extra if(i%2==0) here

        //search all odd numbers between 3 and the (odd ceiling) sqrt of our number
        //if there is perfect division somewhere it's not a prime
        if(hasDivisor(i))
        {
            continue;
        }        
        printf("Number %ld is a prime!\n", i);
    }
    return 0;
}

I'm using Arch Linux x64 with GCC version 8.2.1 and compiling with:

gcc main.c -lm -O3 -o primesearcher

Although I tested with O1 and O2, as well.

I'm "benchmarking" with the command below:

./primesearcher & sleep 10; kill $! 

which runs the program for 10seconds and outputs primes to the terminal for that time and then kills it. I obviously tried allowing the program to run more (30, 60 and 180 seconds) but the results are about 9/10 of the time in favor of the version checking even numbers (modulo 2 version has found a bigger prime before it got killed).

Any idea why would this be happening? Maybe something wrong in terms of code in the end?

sepp2k
  • 363,768
  • 54
  • 674
  • 675
  • 4
    The code is working, so is the question more suitable for CodeReview? – Weather Vane Jan 12 '19 at 11:14
  • 4
    the number 1 is **not** a prime – phuclv Jan 12 '19 at 11:29
  • 3
    Ah please don't make comments look stupid by changing the code you posted. I don't see why you are messing around with the square root. You could have `unsigned long squareRoot = round(sqrt(number));` and it does not matter whether `squareRoot` is odd or even. – Weather Vane Jan 12 '19 at 11:33
  • @WeatherVane since I'm only checking odd numbers for primality and these can't have an even divisor but the sqrt result may result in an even number I thought this should be done (otherwise I would test till < squareRoot and never with = as I'm incrementing by 2). Perhaps you are right though but I doubt it's what causes the problem here. Unfortunately I don't have enough reputation to move the question. I guess I'll ask a mod to move it where you suggested. – user-2147482617 Jan 12 '19 at 11:41
  • @phuclv you are correct. fixed that. – user-2147482617 Jan 12 '19 at 11:42
  • It still has to be `<= squareRoot`. – Weather Vane Jan 12 '19 at 11:48
  • @WeatherVane if squareRoot is let's say for example 12 then j would go 3,5,7,9,11 and then 13 which is bigger than 12. I'm saying that the = won't be executed in some occasions like this. Maybe it won't hurt though. I'll try it soon. – user-2147482617 Jan 12 '19 at 11:52
  • How can you go to `13` when the square root is `12`? And why would you need to? – Weather Vane Jan 12 '19 at 11:56
  • 1
    It doesn't matter if you hit the limit exactly. You only need to stop when you are larger than the limit. Whether your last check is `limit-1` or `limit` does not make any difference. You can only hit one of them anyway. – Gerhardh Jan 12 '19 at 11:57
  • 1
    To answer why one version of the program runs faster than another, we would need to look at both versions. On one hand, you have not presented the version of your program you claim is faster, and on the other hand you have presented *multiple* versions of the one you claim is slower. This won't do. Since you seem to be focused broadly on optimization rather than on one specific change, I think you would be better served by a [Code Review](https://codereview.stackexchange.com/) with that focus than by the present question, as indeed WeatherVane already suggested. – John Bollinger Jan 12 '19 at 12:38
  • `unsigned long squareRoot = floor(sqrt(number));` is weak code. It fails fro large `long` values due to rounding. Further, this code is not needed. – chux - Reinstate Monica Jan 12 '19 at 13:31
  • Why do you think `i++; i++` is faster than `i + = 2`? I'm sure the compiler optimizes both to be the same, but I'm curious as to what your reasoning is. – Mad Physicist Jan 12 '19 at 14:27
  • @WeatherVane I do not recommend [`unsigned long squareRoot = round(sqrt(number))`](https://stackoverflow.com/questions/54158929/calculating-primes-using-only-odd-divisors-is-slower/54160322#comment95148457_54158929) to due rounding when converting large `number` to `double` - it may round down so far to generate the wrong limit for the prime test. The `round()` call does cover up weak imprecise `sqrt()` functions. An integer square root function is called for here. – chux - Reinstate Monica Jan 12 '19 at 14:40
  • 1
    @chux agreed and the compiler gives a warning too. – Weather Vane Jan 12 '19 at 15:16

1 Answers1

1

Code is slower if(!((squareRoot&1)==0)) than with no test because it rarely has benefit.

Keep in mind that for most number, the iteration limit is never reached due to an early return from the number%j test. Primes tend to become rare as number grows.

An rare extra iteration is not offset by the repetitive cost of the test.

Comparing !((squareRoot&1)==0) to number%2 ==0 is moot.

OP's method of testing speed is error prone when differences are small: "runs a tad bit slower most of the times" shows the inconsistency.

A huge amount of time is in the printf(). To compare prime computation performance, I/O needs to be eliminated.

The kill is not that precise either.

Instead, form a loop to stop when i reaches a value like 4,000,000,000 and time that.


Code has other weaknesses:

unsigned long squareRoot = floor(sqrt(number)); can creates the wrong root for large number due to rounding when converting number to double and imprecision of the sqrt() routine. OP's reference addresses the mathematical algorithm need. Yet this C code implementation can readily fail given the limitations of real computations.

Instead, suggest

// Prime test for all unsigned long number
bool isPrime(unsigned long number) {
  if (number % 2 == 0) {  // This can be eliminated if `number` is always odd.
    return number == 2;
  }

  for (unsigned long j = 3; j <= number/j; j += 2) {
    if (number%j == 0) {
      return false;
    }
  }

  return number > 2;
}

The cost of number/j is often nil with modern compilers as they see number%j and effectively compute both quotient and remainder at once. Thus the limit of j <= squareRoot is achieved 1) without expensive sqrt() calculation 2) is accurate for large number, unlike sqrt() usage.


Use matching specifiers. u, not d for unsigned types.

// printf("Number %ld is a prime!\n", i);
printf("Number %lu is a prime!\n", i);

Use of a global i here is a weak coding style. Suggest re-coding and pass by function instead.


For more substantial improvements, look to Sieve of Eratosthenes and keeping a list of formerly found primes and test those rather than all odd numbers.

Prime testing is a deep subject with many more advanced ideas.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • Thanks for answering. Unfortunately I could not get a better execution time for the only odds number checker. I've made an online diff here to see how the code is now with your suggestions applied(one version still checking for evens and one for odds only). I'm now measuring performance with the time command since printf is not used anymore. I've used the hard limit of 45,000,000 because I didn't want for this to take ages. Both versions take 49s to complete again with the odds version being slower for fragments of a second. The diff can be found here: https://www.diffchecker.com/11b12TGD – user-2147482617 Jan 12 '19 at 18:34
  • @user-2147482617 For 45,000,000, Sieve of Eratosthenes would likely be fastest – chux - Reinstate Monica Jan 12 '19 at 18:58
  • @user-2147482617 A not so optimized Sieve of Eratosthenes(45,000,000) took less than 2s. Sieve of Eratosthenes(1000,000,000) took 24s. – chux - Reinstate Monica Jan 12 '19 at 19:21