0

I made a program to find primes below a given number.

number = int(input("Enter number: "))
prime_numbers = [2]  # First prime is needed.
for number_to_be_checked in range(3, number + 1):  
    square_root = number_to_be_checked ** 0.5
    for checker in prime_numbers:  # Checker will become 
           # every prime number below the 'number_to_be_checked'
           # variable because we are adding all the prime numbers 
           # in the 'prime_numbers' list.
        if checker > square_root:
            prime_numbers.append(number_to_be_checked)
            break
        elif number_to_be_checked % checker == 0:
            break
print(prime_numbers)

This program checks every number below the number given as the input. But primes are of the form 6k ± 1 only. Therefore, instead of checking all the numbers, I defined a generator that generates all the numbers of form 6k ± 1 below the number given as the input. (I added 3 also in the prime_numbers list while initializing it as 2,3 cannot be of the form 6k ± 1)

def potential_primes(number: int) -> int:
    """Generate the numbers potential to be prime"""
    # Prime numbers are always of the form 6k ± 1.
    number_for_function = number // 6
    for k in range(1, number_for_function + 1):
        yield 6*k - 1
        yield 6*k + 1

Obviously, the program should have been much faster because I am checking comparatively many less numbers. But, counterintuitively the program is slower than before. What could be the reason behind this?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • you don't show the new code using that smaller list. please edit to include it in your question. this is important, as you could do it in several ways and you probaly did it the less performant way. [mcve] please. – Will Ness Nov 19 '22 at 11:38
  • You should show some exact code of the two conditions with timings. I get that your second code is actually faster (2.06s vs. 1.83s), but maybe I constructed the test differently. – Mark Tolonen Nov 19 '22 at 18:09

3 Answers3

1

In every six numbers, three are even and one is a multiple of 3. The other two are 6-coprime, so are potentially prime:

6k+0    6k+1    6k+2    6k+3    6k+4    6k+5
even            even            even
3x                      3x

For the three evens your primality check uses only one division (by 2) and for the 4th number, two divisions. In all, five divisions that you seek to avoid.

But each call to a generator has its cost too. If you just replace the call to range with the call to create your generator, but leave the other code as is(*), you are not realizing the full savings potential.

Why? Because (*)if that's the case, while you indeed test only 1/3 of the numbers now, you still test each of them by 2 and 3. Needlessly. And apparently the cost of generator use is too high.

The point to this technique known as wheel factorization is to not test the 6-coprime (in this case) numbers by the primes which are already known to not be their divisors, by construction.

Thus, you must start with e.g. prime_numbers = [5,7] and use it in your divisibility testing loop, not all primes, which start with 2 and 3, which you do not need.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
0

Using nested for loop along with square root will be heavy on computation, rather look at Prime Sieve Algorithm which is much faster but does take some memory.

0

One way to use the 6n±1 idea is to alternate step sizes in the main loop by stepping 2 then 4. My Python is not good, so this is pseudocode:

function listPrimes(n)
  // Deal with low numbers.
  if (n < 2) return []
  if (n = 2) return [2]
  if (n = 3) return [2, 3]

  // Main loop
  primeList ← [2, 3]
  limit ← 1 + sqrt(n) // Calculate square root once.
  index ← 5           // We have checked 2 and 3 already.
  step ← 2            // Starting step value: 5 + 2 = 7.
  
  while (index <= limit) {
    if (isPrime(index)) {
      primeList.add(index)
    }
    index ← index + step
    step ← 6 - step      // Alternate steps of 2 and 4
  }
  
  return primeList
end function
rossum
  • 15,344
  • 1
  • 24
  • 38
  • That is a great solution, but my question is that why is my second approach less performant? – PythonBeginner Nov 19 '22 at 11:16
  • Your second approach adds a function call to `potential_primes()`. Any function call has a cost. – rossum Nov 19 '22 at 11:31
  • @rossum _one_ function call has a cost? --- also, what is `isPrime`? – Will Ness Nov 19 '22 at 11:39
  • It is more than one call, you are calling it multiple times every time you need a new potential prime to check. `isPrime` is just a prime checker, returning true or false. – rossum Nov 19 '22 at 12:13
  • `for number_to_be_checked in potential_primes(number + 1):` has only one call to `potential_primes()`, which returns a generator, isn't it? that generator is indeed called multiple times and each call has a cost. but calling that generator is essential to the Q. as to `isPrime`, there are many ways to code it, affecting performance, which was indeed the main issue with the OP problem, I suspect. – Will Ness Nov 19 '22 at 15:26