¿Por qué dos algoritmos para encontrar números primos difieren tanto en velocidad a pesar de que parecen hacer el mismo número de iteraciones?

Tengo dos algoritmos para encontrar primos, en Python. El bucle interno de cada uno parece ejecutarse el mismo número de veces, y es igualmente simple. Sin embargo, uno de ellos toma 10 veces más que el otro. Mi pregunta es

¿Por qué? ¿Es esta una peculiaridad de Python que se puede optimizar (cómo?), O me falta algo más?

El problema que estoy resolviendo es esencialmente dehttp: //www.spoj.pl/problems/PRIME1. En mi caso, tengo N = 10 ** 9, delta = 10 ** 5, y quiero encontrar todos los números primos entre N-delta y delta. Tambien tengosmallprimes, una lista prefabricada de todos los números primos menores o iguales a la raíz cuadrada de N. El primer algoritmo es muy simple:

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) lleva mucho tiempo (unos 10 segundos). El bucle interno se llama 195170 veces. También tengo una versión de este algoritmo que reemplaza el bucle con una comprensión de la lista (esa es la que realmente uso para el perfil; vea el final de la pregunta) pero eso no es más rápido.

a segunda versión es (una implementación fea de) el tamiz 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]

Esto es aproximadamente 10 veces más rápido, toma menos de 2 segundos. Sin embargo, el bucle interno (sillyfun ()) se llama 259982 veces, más que en el último caso. No puedo explicar por qué esto es rápido.

Pensé que tal vez la razón es porque el bucle interno del primer algoritmo contiene aritmética modular, mientras que el segundo solo tiene una asignación. Sin embargo, lo siguiente parece implicar que la asignación no es más rápida que la aritmética modular:

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

Aquí está la versión (menos legible) del primer algoritmo que 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

Aquí están el resultado de llamar al generador de perfiles para range_f2 () (tenga en cuenta el número de veces que se evalúa la expresión generadora):

>>> 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}

Aquí está el resultado del generador de perfiles 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, aquí está el código completo que genera las listas de primos pequeños (de una manera muy tonta) para que pueda verificar qué resultados obtiene:http: //pastebin.com/7sfN4mG

ACTUALIZA Por demanda popular, los datos de perfil para el primer fragmento de código. No hay datos sobre cuántas veces se ejecuta el bucle interno, pero parece bastante claro que es lo mismo que el tercero.

>>> 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}

Respuestas a la pregunta(2)

Su respuesta a la pregunta