or que dois algoritmos para encontrar números primos diferem tanto na velocidade, mesmo que pareçam fazer o mesmo número de iteraçõe

Tenho dois algoritmos para encontrar números primos, em Python. O loop interno de cada um parece ser executado o mesmo número de vezes e é igualmente simples. No entanto, um deles leva 10 vezes mais que o outro. Minha pergunta é

Por quê? Isso é uma peculiaridade do Python que pode ser otimizada (como?) Ou estou perdendo outra coisa?

O problema que estou resolvendo é essencialmente dehttp: //www.spoj.pl/problems/PRIME1. No meu caso, tenho N = 10 ** 9, delta = 10 ** 5 e quero encontrar todos os números primos entre N-delta e delta. Eu também tenhosmallprimes, uma lista pré-criada de todos os números primos menores ou iguais à raiz quadrada de N. O primeiro algoritmo é muito simples:

def range_f1(lo, hi, smallprimes):
  """Finds all primes p with lo <= p <= hi. 

  smallprimes is the sorted list of all primes up to (at least) square root of hi.
  hi & lo might be large, but hi-lo+1 miust fit into a long."""

  primes =[]
  for i in xrange(hi-lo+1):
    n = lo + i

    isprime = True
    for p in smallprimes:
        if n % p == 0:
            isprime = False
            break

    if isprime:
        primes.append(n)

  return primes

Callingrange_f1(N-delta,N,smallprimes) demora muito tempo (cerca de 10 segundos). O loop interno é chamado 195170 vezes. Também tenho uma versão desse algoritmo que substitui o loop por uma compreensão de lista (essa é a que eu realmente uso para criação de perfil; veja o final da pergunta), mas isso não é mais rápid

A segunda versão é (uma feia implementação) da peneira de Eratóstenes:

def range_sieve(lo, hi, smallprimes):
  """Parameters as before"""

  # So ugly! But SO FAST!! How??

  delta = hi-lo+1
  iamprime = [True] * delta      # iamprime[i] says whether lo + i is prime
  if lo<= 1:
    iamprime[1 - lo] = False

  def sillyfun():      # For profiling & speed comparison
    pass

  for p in smallprimes:
    rem = lo % p
    if rem == 0:
        iamprime[0] = False
    for i in xrange(p - rem, delta, p):
        iamprime[i] = False
        sillyfun()

    if p >= lo and p <= hi:
        iamprime[p - lo] = True

  return [p + lo for (p, ami) in enumerate(iamprime) if ami]

Isso é cerca de 10 vezes mais rápido, leva menos de 2 segundos. No entanto, o loop interno (sillyfun ()) é chamado 259982 vezes, mais do que no último caso. Não sei explicar por que isso é rápido.

Pensei que talvez o motivo seja porque o loop interno do primeiro algoritmo contém aritmética modular, enquanto o segundo apenas possui uma atribuição. No entanto, o seguinte parece sugerir que a atribuição não é mais rápida que a aritmética modular:

>>> from timeit import timeit
>>> timeit("10**9 % 2341234")
0.23445186469234613
>>> timeit("a[5000]=False", "a = [True] * 10000")
0.47924750212666822

Aqui está a versão (menos legível) do primeiro algoritmo que eu realmente uso:

def range_f2(lo, hi, smallprimes):

  primes =[]
  for i in xrange(hi-lo+1):
    n = lo + i

    try:
        (1 for p in smallprimes if n % p ==0).next()
    except StopIteration:
        primes.append(n)

  return primes

Aqui estão o resultado de chamar o criador de perfil para range_f2 () (observe o número de vezes que a expressão da geração é avaliada):

>>> from cProfile import run as prof
>>> prof("range_f2(N-delta,N,sp)")
 200005 function calls in 13.866 CPU seconds

 Ordered by: standard name

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000   13.866   13.866 <string>:1(<module>)
 195170   12.632    0.000   12.632    0.000 prime1.py:101(<genexpr>)
      1    1.224    1.224   13.865   13.865 prime1.py:90(range_f2)
   4832    0.009    0.000    0.009    0.000 {method 'append' of 'list' objects}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Aqui está o resultado do criador de perfil para range_sieve ():

>>> prof("range_sieve(N-delta,N,sp)")
259985 function calls in 1.370 CPU seconds

Ordered by: standard name
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.003    0.003    1.370    1.370 <string>:1(<module>)
     1    0.877    0.877    1.367    1.367 prime1.py:39(range_sieve)
259982    0.490    0.000    0.490    0.000 prime1.py:51(sillyfun)
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Finalmente, aqui está o código completo que gera as listas de pequenos números primos (de uma maneira muito boba), para que você possa verificar quais resultados obtém:http: //pastebin.com/7sfN4mG

ATUALIZA Por demanda popular, os dados de criação de perfil para o primeiro pedaço de código. Não há dados sobre quantas vezes o loop interno é executado, mas parece bastante claro que é o mesmo que o terceir

>>> prof("range_f1(N-delta,N,sp)")
      4835 function calls in 14.184 CPU seconds
Ordered by: standard name
ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     1    0.000    0.000   14.184   14.184 <string>:1(<module>)
     1   14.162   14.162   14.183   14.183 prime1.py:69(range_f1)
  4832    0.021    0.000    0.021    0.000 {method 'append' of 'list' objects}
     1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

questionAnswers(1)

yourAnswerToTheQuestion