Why is equivalent Python code so much slower

前端 未结 4 1690
情歌与酒
情歌与酒 2020-12-15 19:09

can somebody explain why is the following trivial code (implementation of Euclid\'s algorithm to find greatest common denominator) about 3 times slower then equivalent code

4条回答
  •  长情又很酷
    2020-12-15 19:28

    Summary

    "Because the function call overhead in Python is much larger than in Ruby."

    Details

    Being a microbenchmark, this really doesn't say much about the performance of either language in proper use. Likely you would want to rewrite the program to take advantage of the strengths of Python and Ruby, but this does illustrate one of the weak points of Python at the moment. The root cause of the speed differences come from function call overhead. I made a few tests to illustrate. See below for code and more details. For the Python tests, I used 2000 for both gcd parameters.

    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
    

    This tells us that it's not the calculation made by the gcd function that contributes most to the time difference, it's the function call itself. With Python 3.1, the difference is similar:

    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
    

    Again, the actual calculation is not biggest contributor, it's the function call itself. In Ruby, the function call overhead is much smaller. (Note: I had to use smaller parameters (200) for the Ruby version of the programs because the Ruby profiler really slows down real-time performance. That doesn't affect CPU time performance, though.)

    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
    

    Notice how neither Ruby 1.8 nor 1.9 suffer greatly from the gcd function call – the function call vs. inline version are more or less equal. Ruby 1.9 seems to be a little better with less difference between the function call and inline versions.

    So the answer to the question is: "because the function call overhead in Python is much larger than in Ruby".

    Code

    # 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
    

    Test runs

    Finally, the commands used to run Python and Ruby with profiling to get the numbers for comparison were pythonX.X -m cProfile iter_gcdX.py 2000 2000 for Python and rubyX.X -rprofile iter_gcdX.rb 200 200 for Ruby. The reason for the difference is that the Ruby profiler adds a lot of overhead. The results are still valid because I'm comparing the difference between a function call and inline code, not the difference between Python and Ruby as such.

    See also

    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

提交回复
热议问题