Почему эквивалентный код Python намного медленнее - PullRequest
19 голосов
/ 29 ноября 2010

Может кто-нибудь объяснить, почему следующий тривиальный код (реализация алгоритма Евклида для нахождения наибольшего общего знаменателя) примерно в 3 раза медленнее, чем эквивалентный код в Ruby?

содержимое iter_gcd.py:

from sys import argv,stderr

def gcd(m, n):
    if n > m:
        m, n = n, m
    while n != 0:
        rem = m % n
        m = n
        n = rem
    return m

# in Python3 code there is xrange replaced with range function
def main(a1, a2):
    comp = 0
    for j in xrange(a1, 1, -1):
        for i in xrange(1, a2):
            comp += gcd(i,j)

    print(comp)

if __name__ == '__main__':
    if len(argv) != 3:
        stderr.write('usage: {0:s} num1 num2\n'.format(argv[0]))
        exit(1)
    else:
        main(int(argv[1]), int(argv[2]))

содержимое iter_gcd.rb:

def gcd(m, n)
    while n != 0
        rem = m % n
        m = n
        n = rem
    end
    return m
end

def main(a1, a2)
    comp = 0
    a1.downto 2 do
        |j|
        1.upto (a2 - 1) do
            |i|
            comp += gcd(i,j)
        end
    end
    puts comp
end

 if __FILE__ == $0
    if ARGV.length != 2
        $stderr.puts('usage: %s num1 num2' % $0)
        exit(1)
    else
        main(ARGV[0].to_i, ARGV[1].to_i)
    end
end

Время выполнения измерений:

$ time python iter_gcd.py 4000 3000
61356305

real    0m22.890s
user    0m22.867s
sys     0m0.006s

$ python -V
Python 2.6.4


$ time python3 iter_gcd.py 4000 3000
61356305

real    0m18.634s
user    0m18.615s
sys     0m0.009s

$ python3 -V
Python 3.1.2


$ time ruby iter_gcd.rb 4000 3000
61356305

real    0m7.619s
user    0m7.616s
sys     0m0.003s

$ ruby -v
ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]

Просто любопытно, почему я получил такие результаты. Я считал, что CPython быстрее в большинстве случаев, чем MRI и даже новый Ruby 1.9 на YARV, но этот «микробенчмарк» действительно удивил меня.

Кстати, я знаю, что могу использовать специализированную библиотечную функцию, такую ​​как fractions.gcd, но я бы хотел сравнить реализации таких базовых и тривиальных языковых конструкций.

Я что-то упустил или реализация следующего поколения Ruby настолько улучшена в области чистой скорости?

Ответы [ 4 ]

36 голосов
/ 08 декабря 2010

Сводка

"Поскольку издержки при вызове функции в Python намного больше, чем в Ruby."

Подробности

Будучи микробенчмарком, это действительно мало что говорито производительности любого языка в правильном использовании.Вероятно, вы захотите переписать программу, чтобы воспользоваться преимуществами Python и Ruby, но это иллюстрирует одно из слабых мест Python на данный момент.Основная причина различий в скорости связана с накладными расходами при вызове функций.Я сделал несколько тестов, чтобы проиллюстрировать.Ниже приведен код и более подробная информация.Для тестов Python я использовал 2000 для обоих параметров gcd.

Interpreter: Python 2.6.6
Program type: gcd using function call
Total CPU time: 29.336 seconds

Interpreter: Python 2.6.6
Program type: gcd using inline code
Total CPU time: 13.194 seconds

Interpreter: Python 2.6.6
Program type: gcd using inline code, with dummy function call
Total CPU  time: 30.672 seconds

Это говорит нам о том, что вычисление, выполняемое функцией gcd, больше всего влияет на разницу во времени, а сам вызов функции.В Python 3.1 разница аналогична:

Interpreter: Python 3.1.3rc1
Program type: gcd using function call
Total CPU time: 30.920 seconds

Interpreter: Python 3.1.3rc1
Program type: gcd using inline code
Total CPU time: 15.185 seconds

Interpreter: Python 3.1.3rc1
Program type: gcd using inline code, with dummy function call
Total CPU time: 33.739 seconds

Опять же, фактический расчет не самый большой вклад, это сам вызов функции.В Ruby накладные расходы на вызов функции намного меньше.(Примечание: мне пришлось использовать меньшие параметры (200) для версий программ на Ruby, потому что профилировщик Ruby действительно замедляет производительность в реальном времени. Однако это не влияет на производительность процессорного времени.)

Interpreter: ruby 1.9.2p0 (2010-08-18 revision 29036) [i486-linux]
Program type: gcd using function call
Total CPU time: 21.66 seconds

Interpreter: ruby 1.9.2p0 (2010-08-18 revision 29036) [i486-linux]
Program type: gcd using inline code
Total CPU time: 21.31 seconds

Interpreter: ruby 1.8.7 (2010-08-16 patchlevel 302) [i486-linux]
Program type: gcd using function call
Total CPU time: 27.00 seconds

Interpreter: ruby 1.8.7 (2010-08-16 patchlevel 302) [i486-linux]
Program type: gcd using inline code
Total CPU time: 24.83 seconds

Обратите внимание, что ни Ruby 1.8, ни 1.9 не сильно страдают от вызова функции gcd - вызов функции и встроенная версия более или менее равны.Ruby 1.9 кажется немного лучше, с меньшей разницей между вызовом функции и встроенными версиями.

Таким образом, ответ на вопрос: «потому что издержки вызова функции в Python намного больше, чем в Ruby».

Код

# iter_gcd -- Python 2.x version, with gcd function call
#             Python 3.x version uses range instead of xrange
from sys import argv,stderr

def gcd(m, n):
    if n > m:
        m, n = n, m
    while n != 0:
        rem = m % n
        m = n
        n = rem
    return m

def main(a1, a2):
    comp = 0
    for j in xrange(a1, 1, -1):
        for i in xrange(1, a2):
            comp += gcd(i,j)
    print(comp)

if __name__ == '__main__':
    if len(argv) != 3:
        stderr.write('usage: {0:s} num1 num2\n'.format(argv[0]))
        exit(1)
    else:
        main(int(argv[1]), int(argv[2]))

# iter_gcd -- Python 2.x version, inline calculation
#             Python 3.x version uses range instead of xrange
from sys import argv,stderr

def main(a1, a2):
    comp = 0
    for j in xrange(a1, 1, -1):
        for i in xrange(1, a2):
            if i < j:
                m, n = j, i
            else:
                m, n = i, j
            while n != 0:
                rem = m % n
                m = n
                n = rem
            comp += m
    print(comp)

if __name__ == '__main__':
    if len(argv) != 3:
        stderr.write('usage: {0:s} num1 num2\n'.format(argv[0]))
        exit(1)
    else:
        main(int(argv[1]), int(argv[2]))

# iter_gcd -- Python 2.x version, inline calculation, dummy function call
#             Python 3.x version uses range instead of xrange
from sys import argv,stderr

def dummyfunc(n, m):
    a = n + m

def main(a1, a2):
    comp = 0
    for j in xrange(a1, 1, -1):
        for i in xrange(1, a2):
            if i < j:
                m, n = j, i
            else:
                m, n = i, j
            while n != 0:
                rem = m % n
                m = n
                n = rem
            comp += m
            dummyfunc(i, j)
    print(comp)

if __name__ == '__main__':
    if len(argv) != 3:
        stderr.write('usage: {0:s} num1 num2\n'.format(argv[0]))
        exit(1)
    else:
        main(int(argv[1]), int(argv[2]))

# iter_gcd -- Ruby version, with gcd function call

def gcd(m, n)
    if n > m
        m, n = n, m
    end
    while n != 0
        rem = m % n
        m = n
        n = rem
    end
    return m
end

def main(a1, a2)
    comp = 0
    a1.downto 2 do
        |j|
        1.upto a2-1 do
            |i|
            comp += gcd(i,j)
        end
    end
    puts comp
end

 if __FILE__ == $0
    if ARGV.length != 2
        $stderr.puts('usage: %s num1 num2' % $0)
        exit(1)
    else
        main(ARGV[0].to_i, ARGV[1].to_i)
    end
end

# iter_gcd -- Ruby version, with inline gcd

def main(a1, a2)
    comp = 0
    a1.downto 2 do |j|
        1.upto a2-1 do |i|
            m, n = i, j
            if n > m
                m, n = n, m
            end
            while n != 0
                rem = m % n
                m = n
                n = rem
            end
            comp += m
        end
    end
    puts comp
end

 if __FILE__ == $0
    if ARGV.length != 2
        $stderr.puts('usage: %s num1 num2' % $0)
        exit(1)
    else
        main(ARGV[0].to_i, ARGV[1].to_i)
    end
end

Тестовые прогоны

Наконец, команды, используемые для запуска Python и Ruby с профилированием для получения чисел для сравнения, были pythonX.X -m cProfile iter_gcdX.py 2000 2000 для Python и rubyX.X -rprofile iter_gcdX.rb 200 200 для Ruby.Причина разницы в том, что профилировщик Ruby добавляет много накладных расходов.Результаты все еще действительны, потому что я сравниваю разницу между вызовом функции и встроенным кодом, а не разницу между Python и Ruby как таковым.

См. Также

Почему Pythonмедленнее по сравнению с Ruby даже с этим очень простым «тестом»?

Что-то не так с этим кодом Python, почему он работает так медленно по сравнению с ruby?

Игра по тестированию компьютерного языка

Поиск в Google: вызов функции ruby ​​python быстрее

22 голосов
/ 29 ноября 2010

Я могу подтвердить, что ruby1.9 быстрее, чем CPython для этого «микробенчмарка» на моей машине :

| Interpreter                     | Time, s | Ratio |
|---------------------------------+---------+-------|
| python-2.6 (cython_gcd.gcd_int) |     2.8 |  0.33 |
| pypy-1.4                        |     3.5 |  0.41 |
| jython-2.5 (java "1.6.0_20")    |     4.7 |  0.55 |
| python-2.6 (cython_gcd.gcd)     |     5.6 |  0.65 |
| ruby-1.9                        |     8.6 |  1.00 |
| jython-2.5                      |     8.9 |  1.03 |
| python-3.2                      |    11.0 |  1.28 |
| python-2.6                      |    15.9 |  1.85 |
| ruby-1.8                        |    42.6 |  4.95 |
#+TBLFM: $3=$2/@6$2;%.2f

Profiler (python -mcProfile iter_gcd.py 4000 3000) показывает, что 80%время, потраченное на вызов функции gcd(), поэтому разница действительно в функции gcd().

Я написал расширение cython_gcd для Python с использованием Cython, cython_gcd.pyx:

def gcd(m, n):
    while n:
        n, m = m % n, n
    return m

def gcd_int(int m, int n):
    while n:
        n, m = m % n, n
    return m

Он используется в iter_gcd.py следующим образом from cython_gcd import gcd, gcd_int.

Чтобы попробовать расширение, запустите: python setup.py build_ext --inplace, где setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("cython_gcd", ["cython_gcd.pyx"])]

setup(
  name = 'Hello world app',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

Чтобы установить расширение глобально, запустите python setup.py install.

10 голосов
/ 29 ноября 2010

Кажется, я помню, что ruby ​​обрабатывает целые числа не так, как Python, поэтому я думаю, что просто Python тратит много времени на выделение памяти, а Ruby просто мутирует целые числа на месте.

Что бы это ни стоило, использование Pypy 1.4 сокращает время выполнения версии Python в моей системе примерно с 15 до 3 секунд.

0 голосов
/ 07 декабря 2010

Я не могу повторить ваш результат.Код Python выглядит в 4 раза быстрее, чем код Ruby:

2010-12-07 13:49:55:~/tmp$ time python  iter_gcd.py 4000 3000
61356305

real    0m14.655s
user    0m14.633s
sys 0m0.012s

2010-12-07 13:43:26:~/tmp$ time ruby iter_gcd.rb 4000 3000
iter_gcd.rb:14: warning: don't put space before argument parentheses
61356305

real    0m54.298s
user    0m53.955s
sys 0m0.028s

Версии:

2010-12-07 13:50:12:~/tmp$ ruby --version
ruby 1.8.7 (2010-06-23 patchlevel 299) [i686-linux]
2010-12-07 13:51:52:~/tmp$ python --version
Python 2.6.6

Кроме того, код Python можно сделать на 8% быстрее:

def gcd(m, n):
    if n > m:
        m, n = n, m
    while n:
        n, m = m % n, n
    return m

def main(a1, a2):
    print sum(
        gcd(i,j)
        for j in xrange(a1, 1, -1)
        for i in xrange(1, a2)
    )

if __name__ == '__main__':
    from sys import argv
    main(int(argv[1]), int(argv[2]))

Позже: когда я устанавливаю и использую ruby ​​1.9.1, код ruby ​​работает намного быстрее:

2010-12-07 14:01:08:~/tmp$ ruby1.9.1 --version
ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]
2010-12-07 14:01:30:~/tmp$ time ruby1.9.1 iter_gcd.rb 4000 3000
61356305

real    0m12.137s
user    0m12.037s
sys 0m0.020s

Я думаю, что ваш вопрос действительно таков: «Почему ruby ​​1.9.xнамного быстрее чем ruby ​​1.8.x? "

...