Почему генерация массива другим способом приводит к значительному ускорению работы другой части кода? - PullRequest
3 голосов
/ 04 июля 2019

Я столкнулся с очень любопытным случаем значительного ускорения в коде после изменения, казалось бы, незначительной детализации.У меня есть следующий код, который является реализацией алгоритма Боровейна для вычисления факториала, реализованного в SageMath (но кроме некоторых незначительных вещей, таких как ^ для возведения в степень, он должен работать в чистом Python 2.7)

def sieve(n): #My implementation of the sieve of Eratosthenes
    T = [1]*n
    for i in xrange(2,n):
        if T[i]==1:
            for j in xrange(2,ceil(n/i)):
                T[i*j]=0
    return [i for i in xrange(2,n) if T[i]]
def expp(n,p): #Exponent of p in the factorization of n!
    k = p
    s = 0
    while k<=n:
        s += n//k
        k = k*p
    return s
def quick_prod(T): #Computing product of the elements of an array using binary splitting
    if len(T)==1:
        return T[0]
    if len(T)==2:
        return T[0]*T[1]
    if len(T)>2:
        s = len(T)//2
        return quick_prod(T[0:s])*quick_prod(T[s:len(T)])

n = 10^6
P = sieve(n) #Array of primes up to n
exps = [expp(n,p) for p in P] #exponents of all primes in P
l = len(bin(abs(n)))-2
nums = [quick_prod([P[j] for j in xrange(len(P)) if (exps[j] >> i)%2])^(2^i) for i in range(l)] #Array of numbers appearing in Borwein's algorithm, whose product is n!
quick_prod(nums)

(Извините за мои ужасные соглашения об именах (и, возможно, другие плохие практики кодирования), я - любитель и на самом деле кодирую вещи "быстро и грязно")

Я не ожидал, что этот код будет особенно эффективным,поэтому я не был удивлен, увидев, что это заняло 10 минут, чтобы бежать.Но когда я начал возиться с кодом, чтобы попытаться улучшить его, я заметил, что замена строки P = sieve(n) на P = prime_range(n) (которая производит тот же массив, за исключением того, что использует функцию, встроенную в SageMath) сократил время выполнения до 3,5 секунд.

Теперь, когда я увидел это, моей первой мыслью было, что объяснение очевидно - моя реализация сита должна быть настолько ужасной, что потребовалась целая вечность, а prime_range делаетэто намного эффективнее.Но результаты меня удивили - sieve(10^6) заняло 4 секунды, а prime_range(10^6) - 2 секунды.Это даже не близко к объяснению разницы в 10 минут!

Некоторые идеи, которые я и мои друзья могли объяснить, могут:

  • Два массива могут быть разными,например, они могут быть заказаны по-разному.Это не так, поскольку sieve(10^6)==prime_range(10^6) возвращает True
  • Несмотря на равенство, массивы могут храниться в разных типах.type(...) возвращает list для обоих.
  • Промежуточные результаты получают в кеше.Вероятно, это не так, поскольку результаты примерно одинаковы, независимо от порядка компиляции, даже после перезапуска ядра.

Единственный способ такого значительного ускорения (или замедления, в зависимости от того, как выПосмотрите на это), может случиться так, если исходный код каким-то образом вернется к тому способу, которым P был сгенерирован после его вычисления.Что может объяснить это поведение?

Ответы [ 3 ]

0 голосов
/ 05 июля 2019

Как вы можете видеть в исходном коде , prime_range является функцией Cython , что означает, что чистый код C генерируется из этого с использованием целых чисел C.

cpdef prime_range(start, stop=None, algorithm="pari_primes", bint py_ints=False):

Кроме того, он использует сверхбыструю библиотеку Pari для большой грязной работы.Так что да, это должно быть намного, намного быстрее.

Когда мы пытаемся время вашего кода, на самом деле это занимает от 1 до 2 секунд;timeit('prime_range(10^6)') дает больше как 12 миллисекунд.Все еще не так плохо, и, очевидно, не несет ответственности за все ваши медленные сроки.

Так что user2357112 и Alain T. правы;Ваш тип все еще Python int s.Это неуловимо - xrange и range возвращают int с, в то время как srange (для "Sage range") вернет <type 'sage.rings.integer.Integer'>, что имеет множество доступных пользовательских методов. Испытание вашего кода с этим изменением таким образом, чтобы мы могли легко его рассчитать. дает гораздо более управляемый результат менее чем за 10 секунд.

Это раздражает, но я обещаю, что это особенность, а неЖук!Но знание того, когда Sage Integer s и Python int s используются, является хорошей практикой, чтобы знать об использовании Sage.Он основан на Python, но это не совсем то же самое.


Наконец, в будущем я бы порекомендовал использовать встроенные Sage, когда это возможно.Они не все оптимизированы, но обычно они намного лучше, чем все, что можно сделать наивно.Не то, чтобы пытаться кодировать это не полезное упражнение!Удачи.

0 голосов
/ 05 июля 2019

Во-первых, как у вас эти 10 минут?

На моей машине я не могу получить больше нескольких секунд.Точнее, я определил следующую тестовую функцию:

def test(fun_to_test, type_to_test, n):
     P = [type_to_test(i) for i in fun_to_test(n)]
     exps = [expp(n,p) for p in P]
     l = len(bin(abs(n)))-2
     nums = [quick_prod([P[j] for j in xrange(len(P)) if (exps[j] >> i)%2])^(2^i) for i in range(l)]
     return quick_prod(nums)

и получаю эти значения времени:

sage: %time r0 = test(sieve, int, n)
CPU times: user 5.47 s, sys: 62.7 ms, total: 5.53 s
Wall time: 5.53 s
sage: %time r1 = test(prime_range, int, n)
CPU times: user 4.39 s, sys: 34.3 ms, total: 4.43 s
Wall time: 4.43 s
sage: %time r2 = test(sieve, Integer, n)
CPU times: user 1.95 s, sys: 50.4 ms, total: 2 s
Wall time: 1.99 s
sage: %time r3 = test(prime_range, Integer, n)
CPU times: user 917 ms, sys: 28.6 ms, total: 945 ms
Wall time: 936 ms

Вы можете увидеть, что занимает время в вашем коде, как это:

sage: %prun r0 = test(sieve, Integer, n)
         893200 function calls (760228 primitive calls) in 2.057 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.949    0.949    0.987    0.987 <ipython-input-38-92e1b030cb97>:1(sieve)
132993/21    0.509    0.000    0.528    0.025 <ipython-input-38-92e1b030cb97>:15(quick_prod)
        1    0.490    0.490    2.041    2.041 <ipython-input-51-ed2c358da30e>:1(test)
    78498    0.035    0.000    0.035    0.000 <ipython-input-38-92e1b030cb97>:8(expp)
    78498    0.024    0.000    0.039    0.000 other.py:213(__call__)
   446208    0.019    0.000    0.019    0.000 {len}
        1    0.016    0.016    2.057    2.057 <string>:1(<module>)
    78498    0.010    0.000    0.010    0.000 {method 'ceil' of 'sage.rings.rational.Rational' objects}
    78498    0.005    0.000    0.005    0.000 {method 'get' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {bin}
        1    0.000    0.000    0.000    0.000 {range}
        1    0.000    0.000    0.000    0.000 {abs}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
0 голосов
/ 05 июля 2019

SageMath (с которым я не знаком), вероятно, работает как numpy.То есть структура массива и внутренние типы данных, которые он использует (и возвращает), намного более эффективны, чем стандартные списки Python.Это может применяться к спискам и другим вычислениям, которые вы выполняете с ним позже.

Вот пример этого явления (на основе numpy).

import numpy as np
def sieve2(n):
    s       = np.ones(n+1)
    s[4::2] = 0
    s[:2]   = 0
    p = 3
    while p*p<=n:
        if s[p]:s[p*p::p] = 0
        p+=2
    return np.arange(n+1)[s==1]

Эта функция возвращает простые числа до 10^ 6 за 0,013 секунды по сравнению с вашей функцией, которая занимает 0,27 на моем компьютере (примерно в 20 раз быстрее).Функция на основе numpy возвращает массив numpy, который имеет свою собственную реализацию основных функций, таких как сложения, умножения, возведения в степень и т. Д. Это также может иметь место для sageMath, который, возможно, ускорит другие части вашей программы.

Обратите внимание, что большая разница во времени при использовании numpy обусловлена ​​его способностью векторизовать вычисления и использовать графический процессор для параллельного выполнения нескольких операций.SageMath может использовать тот же трюк для своих больших целочисленных вычислений (чего, вероятно, Python не делает)

...