有人能解释一下为什么下面这段琐碎的代码(实现欧几里德算法来寻找最大公分母)比Ruby中的等效代码慢3倍吗?

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更快,甚至比YARV上的新Ruby 1.9更快,但这个"微基准"确实让我感到惊讶.

顺便说一句,我知道我可以使用特殊的库函数,比如分数.gcd,但我想比较一下这种基本的和琐碎的语言 struct 的实现.

是我错过了什么,还是下一代Ruby的实现在速度方面有了很大的改进?

推荐答案

总结

"因为Python中的函数调用开销比Ruby中大得多."

细节

作为一个微基准,这并不能说明这两种语言在正确使用中的表现.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中,函数调用开销要小得多.(注意:对于Ruby版本的程序,我不得不使用较小的参数(200),因为Ruby profiler确实会降低实时性能.不过,这不会影响CPU时间性能.)

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并进行分析以获得比较的命令数,Python为pythonX.X -m cProfile iter_gcdX.py 2000 2000,Ruby为rubyX.X -rprofile iter_gcdX.rb 200 200.造成这种差异的原因是Ruby profiler增加了很多开销.结果仍然有效,因为我比较的是函数调用和内联代码之间的差异,而不是Python和Ruby之间的差异.

另见

Why is python slower compared to Ruby even with this very simple “test”?

Is there something wrong with this python code, why does it run so slow compared to ruby?

The Computer Language Benchmarks Game

Google Search: ruby python function call faster

Python-3.x相关问答推荐

根据样本量随机 Select 组内样本

为什么打印语句在Python多处理脚本中执行两次?

Python将类实例变量转换为嵌套 struct

Django将任何查询显示为html表格

在Pandas 数据帧中为小于5位的邮政编码添加前导零

逐行比较2个Pandas数据帧,并对每一行执行计算

如何使用 Selenium Python 连续单击一个按钮直到另一个元素出现?

列出相同索引的Pandas

pip 找不到最新的软件包版本

Pandas 值列中列表中元素的计数

机器学习实验笔记本的工作区 url

使用一周的特定第一天将每日日期转换为每周

smtplib 在 Python 3.1 中发送带有 unicode 字符的邮件的问题

Python - For 循环数百万行

为什么 virtualenv 会有效地禁用 Python 3 制表符补全?

django - 值更改后自动更新日期

Python中的多行日志(log)记录

如何模拟 open(...).write() 而不会出现没有这样的文件或目录错误?

如何从 Python 3 导入 FileNotFoundError?

Django Rest 框架 ListField 和 DictField