Почему два алгоритма нахождения простых чисел так сильно различаются по скорости, даже если кажется, что они выполняют одинаковое количество итераций? - PullRequest
5 голосов
/ 27 декабря 2011

У меня есть два алгоритма поиска простых чисел в Python.Кажется, что внутренний цикл каждого из них выполняется одинаковое количество раз и одинаково прост.Тем не менее, один из них занимает в 10 раз больше, чем другой.Мой вопрос:

Почему?Это какая-то особенность Python, которую можно оптимизировать (как?), Или я упускаю что-то еще?

Проблема, которую я решаю, по сути, http://www.spoj.pl/problems/PRIME1/. В моем случаеУ меня N = 10 ** 9, дельта = 10 ** 5, и я хочу найти все простые числа между N-дельта и дельта.У меня также есть smallprimes, заранее составленный список всех простых чисел, меньших или равных квадратному корню из N. Первый алгоритм очень прост:

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

Вызов range_f1(N-delta,N,smallprimes) занимает много времени (около 10 секунд).Внутренний цикл называется 195170 раз.У меня также есть версия этого алгоритма, которая заменяет цикл на понимание списка (это та, которую я фактически использую для профилирования; см. Конец вопроса), но это не быстрее.

Вторая версия (уродливая реализация) сита Эратосфена:

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]

Это примерно в 10 раз быстрее, занимает менее 2 секунд.Однако внутренний цикл (sillyfun ()) вызывается 259982 раза, больше, чем в последнем случае.Я затрудняюсь объяснить, почему это быстро.

Я подумал, что, возможно, причина в том, что внутренний цикл первого алгоритма содержит модульную арифметику, в то время как второй имеет только присваивание.Однако следующее, по-видимому, подразумевает, что назначение не быстрее, чем модульная арифметика:

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

Вот (менее читаемая) версия первого алгоритма, который я фактически использую:

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

Вот результат вызова профилировщика для range_f2 () (обратите внимание, что оценивается число генерирующих время выражений):

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

Вот результат профилировщика для 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}

Наконец, вот полный код, который генерирует списки небольших простых чисел (очень глупо), чтобы вы могли проверить, какие результаты вы получите: http://pastebin.com/7sfN4mG4

ОБНОВЛЕНИЕ По многочисленным просьбам данные профилирования для первого куска кода.Нет данных о том, сколько раз выполнялся внутренний цикл, но кажется достаточно ясным, что он такой же, как третий.

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

1 Ответ

3 голосов
/ 27 декабря 2011

Разница алгоритмическая.В первой версии, пробное деление, вы проверяете каждого кандидата на соответствие всем маленьким простым числам - то, что вы не останавливаетесь, когда маленькое простое число превышает candidate ** 0.5, не имеет значения для range(10**9 - 10**5 ,10**9), если у маленьких простых чисел есть хорошая верхняя граница, нобыло бы, если бы длина диапазона была намного больше по отношению к верхней границе.Для композитов это не требует больших затрат, так как у большинства из них есть хотя бы один маленький простой делитель.Но для простых вы должны пройти весь путь до N**0.5.В этом интервале примерно 10**5/log(10**9) простых чисел, каждое из которых делится на пробные значения приблизительно на 10**4.5/log(10**4.5) простых чисел, так что получается примерно 1.47*10**7 пробных делений на простые числа.

С другой стороны, сСито, вы только скрещиваете композиты, каждый композит вычеркивается столько раз, сколько у него простых делителей, а простые числа вообще не скрещиваются (поэтому простые числа свободны).Число простых делителей n ограничено (кратно) log(n) (это грубая верхняя граница, обычно сильно завышенная), так что получается верхняя граница 10**5*log(10**9) (умноженная на небольшую постоянную) пересечений:выкл, около 2*10**6.Кроме того, вычеркивание может быть менее трудоемким, чем деление (не знаю насчет Python, это для C-массивов).Таким образом, вы выполняете меньше работы с ситом.

Редактировать: собрал действительные числа для 10**9-10**5 до 10**9.

Ticks: 259987
4832
Divisions: 20353799
4832

Сито выполняет только 259987 пересечений,Вы видите, что грубая верхняя граница, приведенная выше, сильно переоценена.Для пробного деления требуется более 20 миллионов делений, из них 16433632 делений для простых чисел (x/log x недооценивает число простых чисел, для x = 10**4.5 примерно на 10%), 3435183 используются для чисел 3297 в этом диапазоне, у которых наименьший простой множитель равенбольше чем n**(1/3).

...