Python [Benchmark] Ruby 2.0 真的快了不少,还是我的 Python 代码有问题?

jjym · 2013年07月22日 · 最后由 reboot 回复于 2016年03月25日 · 12046 次阅读
ruby -v
ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-linux]

python3 --version
Python 3.3.2

使用传统的fibonacci来benchmark

#ruby
require 'benchmark'

def fib n
  if n < 2
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end

Benchmark.bm{|bm| bm.report{puts fib(35)}}
#python
import timeit

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

if __name__=='__main__':
    from timeit import Timer
    t = Timer("print(fib(35))","from __main__ import fib")
    print(t.timeit(1))
#ruby

ruby fib.rb
       user     system      total        real
 14930352
  2.600000   0.000000   2.600000 (  2.601639)

#python3.2.2

fib.py 
14930352
9.720147652000378

ruby版本快了大概3.7倍 我使用python2.7.5版会稍微快些,大概6.3秒,当然还是比ruby慢的

ruby2.0真的快了不少,还是说我的python代码写的有问题?

共收到 58 条回复

Ruby2.0确实变快了,内存也比以前占用得更少了。

也改是提高性能的时候啦

ruby 2.0 比 ruby 1.8.7 某些方面快了10倍以上,超过2.7.5是自然的,不过超这么多有点不正常

加了好多特化指令的结果... 你可以把 trace 的编译参数关掉看到可以更快点

require 'benchmark'

iseq = RubyVM::InstructionSequence.compile\
<<-RUBY, __FILE__, '.', 0, trace_instruction: false
def fib n
  if n < 2
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end
RUBY
iseq.eval

Benchmark.bm{|bm| bm.report{puts fib(35)}}

#4楼 @luikore 果然阿。。最速2.27s,关了specialized_instruction基本都要5s多...

现在玩python了啊!

#6楼 @qinjker 没,只是benchmark下..

$ ruby fib.rb 
       user     system      total        real
 14930352
  2.240000   0.000000   2.240000 (  2.240812)
$ python2 fib.py 
14930352
4.98553395271
$ python3 fib.py 
14930352
7.37453122099987
$ pypy fib.py 
14930352
1.59528398514

if ruby puts "u love" elsif pathon puts 'u love too' ... end puts 'good programmers'

#5楼 @jjym 怎么看关掉前后生成的指令有何不同?

#10楼 @bhuztez

pypy 可以和 jruby 对比一下, 两个 BDD (benchmark driven development) 产物...

看指令:

puts iseq.disasm

左列是 offset, 表示偏离的 qword 数

opt_plus 是常见方法的特化指令, 1.9 加的, 做整数运算大约是静态语言的一半速度了

opt_send_simple指令_OP_XXX 是 2.0 加进去的特化指令

#11楼 @luikore

太假了,send改成opt_ltopt_minusopt_plus竟然从和Python速度一样变成比Python快一倍

可是Python现在默认生成的指令也是类似的,对应的是COMPARE_OPBINARY_ADDBINARY_SUBTRACT,而不是先载入operator再CALL_FUNCTION

我觉得原因在其他地方。

#12楼 @bhuztez

这要看对应指令的实现, python 的实现可能只是改了下 calling convention, ruby 的 opt_plus 是把 fixnum / float / string / array 的加法 inline 进来了

#13楼 @luikore

别的没有,刚好就COMPARE_OPBINARY_ADDBINARY_SUBTRACT这三个inline了int的,对于现在这个情形应该一样才对啊。

Python没用threading是会慢一点,但是Ruby的速度快了一倍啊,不是换个threading就能达到的吧。


case BINARY_ADD:
    w = POP();
    v = TOP();
    if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
        /* INLINE: int + int */
        register long a, b, i;
        a = PyInt_AS_LONG(v);
        b = PyInt_AS_LONG(w);
        /* cast to avoid undefined behaviour
           on overflow */
        i = (long)((unsigned long)a + b);
        if ((i^a) < 0 && (i^b) < 0)
            goto slow_add;
        x = PyInt_FromLong(i);
    }
    else if (PyString_CheckExact(v) &&
             PyString_CheckExact(w)) {
        x = string_concatenate(v, w, f, next_instr);
        /* string_concatenate consumed the ref to v */
        goto skip_decref_vx;
    }
    else {
      slow_add:
        x = PyNumber_Add(v, w);
    }
    Py_DECREF(v);
  skip_decref_vx:
    Py_DECREF(w);
    SET_TOP(x);
    if (x != NULL) continue;
    break;

#14楼 @bhuztez

目测区别在 pop()

ruby 的实现基本和 python 的一样, 但没有 pop(), 而是带两个 operand 的 register machine 指令

#15楼 @luikore POP其实挺快的。虽然是stack-based,但是每个函数运行期的frame里都有一个预先分配好的stack,也就是说通常情况(目前情况)下,你可以理解为stack就是一个C数组。所以,POP其实拖慢不了多少的。

#define BASIC_POP()       (*--stack_pointer)
#define POP()                  BASIC_POP()

#16楼 @bhuztez 好吧其实 ruby 的实现也 pop 的

你确定 python 没开 threading? 在解释器中很基本的... switch..case 的状态也可以做 threading 的.


cachegrind 一下比较下 cache miss 比率大致可以猜出有没有做 threading :

valgrind --tool=cachegrind --cachegrind-out-file=/tmp/rb ruby fib.rb
==49280==
==49280== I   refs:      10,237,483,293
==49280== I1  misses:           397,866
==49280== LLi misses:             9,631
==49280== I1  miss rate:           0.00%
==49280== LLi miss rate:           0.00%
==49280==
==49280== D   refs:       4,846,225,267  (2,980,702,999 rd   + 1,865,522,268 wr)
==49280== D1  misses:           456,388  (      344,239 rd   +       112,149 wr)
==49280== LLd misses:            66,417  (       11,651 rd   +        54,766 wr)
==49280== D1  miss rate:            0.0% (          0.0%     +           0.0%  )
==49280== LLd miss rate:            0.0% (          0.0%     +           0.0%  )
==49280==
==49280== LL refs:              854,254  (      742,105 rd   +       112,149 wr)
==49280== LL misses:             76,048  (       21,282 rd   +        54,766 wr)
==49280== LL miss rate:             0.0% (          0.0%     +           0.0%  )

主要看 LL miss

#17楼 @luikore 明显没有,就是直接switch的,上面不就贴代码了

#18楼 @bhuztez

这只能确定对 Visual C 没开 threading label 也没有放到数组中? 搜下 &&BINARY_ADD ...

#17楼 @luikore

Python

==8232== I   refs:      24,435,810,159
==8232== I1  misses:           424,549
==8232== LLi misses:             7,356
==8232== I1  miss rate:           0.00%
==8232== LLi miss rate:           0.00%
==8232== 
==8232== D   refs:      11,447,475,486  (8,210,939,243 rd   + 3,236,536,243 wr)
==8232== D1  misses:           505,807  (      410,181 rd   +        95,626 wr)
==8232== LLd misses:            57,991  (       23,760 rd   +        34,231 wr)
==8232== D1  miss rate:            0.0% (          0.0%     +           0.0%  )
==8232== LLd miss rate:            0.0% (          0.0%     +           0.0%  )
==8232== 
==8232== LL refs:              930,356  (      834,730 rd   +        95,626 wr)
==8232== LL misses:             65,347  (       31,116 rd   +        34,231 wr)
==8232== LL miss rate:             0.0% (          0.0%     +           0.0%  )

Ruby

==8464== I   refs:      14,783,720,041
==8464== I1  misses:           137,316
==8464== LLi misses:             9,038
==8464== I1  miss rate:           0.00%
==8464== LLi miss rate:           0.00%
==8464== 
==8464== D   refs:       6,208,884,291  (3,666,216,719 rd   + 2,542,667,572 wr)
==8464== D1  misses:           205,003  (      150,900 rd   +        54,103 wr)
==8464== LLd misses:            57,501  (       17,151 rd   +        40,350 wr)
==8464== D1  miss rate:            0.0% (          0.0%     +           0.0%  )
==8464== LLd miss rate:            0.0% (          0.0%     +           0.0%  )
==8464== 
==8464== LL refs:              342,319  (      288,216 rd   +        54,103 wr)
==8464== LL misses:             66,539  (       26,189 rd   +        40,350 wr)
==8464== LL miss rate:             0.0% (          0.0%     +           0.0%  )

已经看不懂你们在说什么了。。。。

#21楼 @bhuztez

python 用 switch case 也能做到这个程度啊, 里面肯定有些黑魔法...

刚下好 py3 代码... 还有就是 python 是否有 inline caching? 这个影响也很大的.

python3 里 direct threading 肯定是开了的.

因为

  1. 每个 opcode 的实现最后有个 if (x != NULL) DISPATCH(); break; 然后那个 DISPATCH() 其实可以是 goto *ip++ (incr instruction pointer, goto next instruction)
  2. opcode_targets.h 把各个 opcode 上边标签的内存地址取出来了, 就是做 direct threading 用的.

python 是取 attr 再 apply, non-method attr 没有 cache 的意义, 估计就是 inline method cache 的区别了...

如果在 ruby 源码的 vm_opts.h 里把这些优化选项改成 0 关掉:

#define OPT_INLINE_METHOD_CACHE 0
#define OPT_GLOBAL_METHOD_CACHE 0

make install

应该可以更接近 python3 一点...

#23楼 @luikore

我贴的那个是Python2

3开了2没开,2还比3快不少 ... 不科学啊

http://bugs.python.org/issue4753

肯定是Ruby用了黑魔法...

Python

def fib(n):
    if n < 2:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)
2           0 LOAD_FAST                0 (n)
            3 LOAD_CONST               1 (2)
            6 COMPARE_OP               0 (<)
            9 POP_JUMP_IF_FALSE       16

3          12 LOAD_CONST               2 (1)
           15 RETURN_VALUE        

5     >>   16 LOAD_GLOBAL              0 (fib)
           19 LOAD_FAST                0 (n)
           22 LOAD_CONST               2 (1)
           25 BINARY_SUBTRACT     
           26 CALL_FUNCTION            1
           29 LOAD_GLOBAL              0 (fib)
           32 LOAD_FAST                0 (n)
           35 LOAD_CONST               1 (2)
           38 BINARY_SUBTRACT     
           39 CALL_FUNCTION            1
           42 BINARY_ADD          
           43 RETURN_VALUE        
           44 LOAD_CONST               0 (None)
           47 RETURN_VALUE        
def fib n
  if n < 2
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 2] n<Arg>     
0000 getlocal         n                                               (   1)
0002 putobject        2
0004 opt_lt           <ic:6>
0006 branchunless     12
0008 putobject        1                                               (   2)
0010 leave                                                            (   1)
0011 pop              
0012 putself                                                          (   4)
0013 getlocal         n
0015 putobject        1
0017 opt_minus        <ic:7>
0019 send             :fib, 1, nil, 8, <ic:2>
0025 putself          
0026 getlocal         n
0028 putobject        2
0030 opt_minus        <ic:8>
0032 send             :fib, 1, nil, 8, <ic:4>
0038 opt_plus         <ic:9>
0040 leave            
function fib(n)
  if (n<2) then
    return 1
  end

  return fib(n-1)+fib(n-2)
end
function <fib.lua:1,7> (13 instructions, 52 bytes at 0x1120710)
1 param, 4 slots, 0 upvalues, 1 local, 3 constants, 0 functions
    1   [2] LT          0 0 -1  ; - 2
    2   [2] JMP         2   ; to 5
    3   [3] LOADK       1 -2    ; 1
    4   [3] RETURN      1 2
    5   [6] GETGLOBAL   1 -3    ; fib
    6   [6] SUB         2 0 -2  ; - 1
    7   [6] CALL        1 2 2
    8   [6] GETGLOBAL   2 -3    ; fib
    9   [6] SUB         3 0 -1  ; - 2
    10  [6] CALL        2 2 2
    11  [6] ADD         1 1 2
    12  [6] RETURN      1 2
    13  [7] RETURN      0 1

还Ruby多一条指令...

#24楼 @luikore

Erlang,快得离谱啊,竟然连1秒都不要...

-module(fib).

-export([test/0, fib/1]).

fib(0) ->
    1;
fib(1) ->
    1;
fib(N) ->
    fib(N-1)+fib(N-2).

test() ->
    {T, V} = timer:tc(fib, fib, [35]),
    io:format("~w:~w~n", [T,V]).

#26楼 @bhuztez 就等你说 erlang 最快了... 这个 benchmark 是非常适合做 JIT 的类型...

你的 py3 可能是开发版有些编译参数有区别, 我的机器上 brew 装的 python3 是比 python2 快一点的.

多一条指令完全是因为 python 的 if 语句无值...

#27楼 @luikore 不是无值,是POP_JUMP顺便POP掉了...

erlang也是纯解释执行啊

➜  demo  python fib.py
14930352
4.9382610321
➜  demo  python3 fib.py
14930352
4.950316907998058
➜  demo  pypy fib.py
14930352
0.762974023819

#28楼 @bhuztez 最后那个 LOAD_CONST None 是个死分支还是没删掉?

#30楼 @luikore 根据定义,所有函数最后都会自动加上这个...

我数的是实际会用到的指令。

刚才我妄图把那两个指令删了,好像把Python解释器搞挂了...

可以先用我的orz生成一下bytecode http://xiazheteng.github.io/orz/

from orz.lua.asm import Assembly, Label

from orz.asm import Opcode, StringTable
from orz.symbol import Global, Local, Name, Attribute, calculate_slots

import StringIO
import marshal


def compile_fib(parent, stringtable):
    filename = Name(__file__)
    func_name = Name("fib")

    n = Local("n")
    fib = Global("fib")

    nnames = calculate_slots([n, fib])

    asm = Assembly(
        func_name, *nnames,
        argcount = 1,
        parent = parent)

    asm.set_lineno(1)

    asm.emit(Opcode.LOAD_FAST, n.slot)
    asm.load_const(2)
    asm.emit(Opcode.COMPARE_OP, 0)

    l = Label()

    asm.emit(Opcode.POP_JUMP_IF_FALSE, l)

    asm.load_const(1)
    asm.emit(Opcode.RETURN_VALUE)

    asm.emit(Opcode.LABEL, l)

    asm.emit(Opcode.LOAD_GLOBAL, fib.slot)
    asm.emit(Opcode.LOAD_FAST, n.slot)
    asm.load_const(2)
    asm.emit(Opcode.BINARY_SUBTRACT)
    asm.emit(Opcode.CALL_FUNCTION, 1)

    asm.emit(Opcode.LOAD_GLOBAL, fib.slot)
    asm.emit(Opcode.LOAD_FAST, n.slot)
    asm.load_const(1)
    asm.emit(Opcode.BINARY_SUBTRACT)
    asm.emit(Opcode.CALL_FUNCTION, 1)

    asm.emit(Opcode.BINARY_ADD)
    asm.emit(Opcode.RETURN_VALUE)

    # asm.load_const(None)
    # asm.emit(Opcode.RETURN_VALUE)

    for names in nnames:
        for name in names:
            name.s = stringtable.add(name.name, interned=True)

    func_name.s = stringtable.add(func_name.name, interned=True)

    return asm


def compile_mod():
    stringtable = StringTable()

    filename = Name(__file__)
    fname = Name(__name__)

    fib = Global("fib")

    nnames = calculate_slots([fib])

    asm = Assembly(fname, *nnames)
    asm.set_lineno(1)

    asm.load_const(compile_fib(asm, stringtable))
    asm.emit(Opcode.MAKE_FUNCTION, 0)
    asm.emit(Opcode.STORE_GLOBAL, fib.slot)
    asm.load_const(None)
    asm.emit(Opcode.RETURN_VALUE)

    for names in nnames:
        for name in names:
            name.s = stringtable.add(name.name, interned=True)

    fname.s = stringtable.add(fname.name, interned=True)

    stringtable.close()

    buf = StringIO.StringIO()
    asm.serialize(buf, __file__)
    return marshal.loads(buf.getvalue())


mod = compile_mod()
d = {}
exec mod in d
fib = d['fib']

if __name__=='__main__':
    from timeit import Timer
    t = Timer("print(fib(35))","from __main__ import fib")
    print(t.timeit(1))

#27楼 @luikore

代码分别抄自http://rosettacode.org/wiki/Fibonacci_sequence

AWK SWIProlog和Python差不多

Perl更慢... Tcl和Perl差不多

bc比Perl还慢

目前Octave垫底

lua比Ruby快那么点

boo和Erlang差不多

Mozilla的JS参数选对了比Erlang快,当然了是开了JIT, 参数js -b -m fib.js比Erlang快,参数js -b -j fib.js只能和Ruby比比

目前试过的里面纯解释还是Erlang最快

楼上神仙打架。

#22楼 @jjym ...

其实闹这么多不如加个 memoize ...

require 'benchmark'
require "memoize"
include Memoize

def fib n
  if n < 2
    1
  else
    fib(n - 1) + fib(n - 2)
  end
end
memoize :fib

Benchmark.bm{|bm| bm.report{puts fib(35)}}

之前从1.9.3要升级到2.0,后来发现2.0的IO处理方面比1.9.3慢了不少,所以又退回用1.9.3了。

#34楼 @luikore 现在是为了比较速度啊,明显运行速度越慢越容易比出来啊,运行速度太快,误差都比运行时间长就坑爹了。

JavaScript开了JIT才比Erlang快没多少,这怎么解释... Pypy开了JIT还不如Erlang

Ruby的字节码和Python几乎完全一样,但是速度确比Python快了一倍,都赶上lua了,这怎么解释。

#34楼 @luikore 👍 效果拔群!!

#36楼 @bhuztez erlang会不会有这种缓存,毕竟无副作用?

#35楼 @rasefon IO完全不关语言的事啊... 难道不是操作系统或者网络的问题? 有具体代码或者例子么?

#36楼 @bhuztez 不知道, 大概是 python vm 里指定了 register modifier 的变量影响了流水线, 导致空跑几个时钟周期之类的... 具体只能 micro benchmark 单个指令了吧...

JIT 比解释慢没什么好奇怪的, java 1.1 刚加 JIT 的时候是默认不开的, 因为开了反而更慢...

erlang 生成了什么样的 byte code 也不知道啊, 或许发现这个函数是 pure recursive 的把 memoize 应用上了...

#39楼 @luikore

{function, fib, 1, 2}.
  {label,1}.
    {line,[{location,"fib.erl",5}]}.
    {func_info,{atom,fib},{atom,fib},1}.
  {label,2}.
    {test,is_integer,{f,4},[{x,0}]}.
    {select_val,{x,0},{f,4},{list,[{integer,1},{f,3},{integer,0},{f,3}]}}.
  {label,3}.
    {move,{integer,1},{x,0}}.
    return.
  {label,4}.
    {allocate_zero,1,1}.
    {line,[{location,"fib.erl",10}]}.
    {gc_bif,'-',{f,0},1,[{x,0},{integer,1}],{x,1}}.
    {move,{x,0},{y,0}}.
    {move,{x,1},{x,0}}.
    {line,[{location,"fib.erl",10}]}.
    {call,1,{f,2}}.
    {line,[{location,"fib.erl",10}]}.
    {gc_bif,'-',{f,0},1,[{y,0},{integer,2}],{x,1}}.
    {move,{x,0},{y,0}}.
    {move,{x,1},{x,0}}.
    {line,[{location,"fib.erl",10}]}.
    {call,1,{f,2}}.
    {line,[{location,"fib.erl",10}]}.
    {gc_bif,'+',{f,0},1,[{y,0},{x,0}],{x,0}}.
    {deallocate,1}.
    return.

#40楼 @bhuztez 那大概就是寄存器机和栈机的区别了... 栈机实现简单, 指令占用空间小, 寄存器机实现略复杂, 但可以指令执行需要的周期少 (可以少做一些 push / pop), 可以利用 cpu pipeline 一次执行好多个(机器码)指令

#41楼 @luikore lua虽然是栈,就是基本不做push/pop的

Python和Ruby通常情况都是相当于是每个函数独立分别固定大小的内存作为栈的,和寄存器机并没有显著区别。

#42楼 @bhuztez lua 就是典型的寄存器机...

#43楼 @luikore 语义上函数调用和返回都是栈啊

#44楼 @bhuztez 语义上就不是, vm register 访问是随机的, 还可以 spill 到真 register 上

#45楼 @luikore 不是寄存器机的语义啊,我说的是lua函数调用的语义啊 ...

#39楼 @luikore 我也觉得应该不管语言本身的问题,可能是IO库的问题。代码太多了,简单说就是cpp/h生成器,用一个内部dsl生成对应的cpp,会生成大约10k个文件,在IO写文件阶段比1.9.3慢了不少,也有可能是erb的问题,具体还没仔细查过因为没时间。

#46楼 @bhuztez 在 lua 里 +, -, < 都不是 call 或者 call 的特化...

而 lua 的

call A, B, C

参数用到了从 A 起的连续 B 个(VM)寄存器, 返回值放到从 A 起的连续 C 个(VM)寄存器里 但是这些 VM 寄存器跑完一趟 mem2reg 完全可以对应到真机寄存器, 不一定在栈里的

是说,能选这个问题试试看吗? http://blog.dhananjaynene.com/2008/07/performance-comparison-c-java-python-ruby-jython-jruby-groovy/ 因为这个的问题是OO问题,跟物件导向語言比较相關。

#49楼 @lulalala 这篇博客太囧了

他说 Java 比 C++ 快, 但他代码里 C++ 的循环了 1000_000 次, 是其他代码循环遍数的 10 倍...

对于斐波那契数列计算的优化是经典问题,楼主运行一下fib 10000 试试。

附赠matrix实现

def fib n
  m = Matrix[[1,1],[1,0]]
  (m ** n)[0,0]
end

ruby逆袭了???

#51楼 @nouse 嗯, 矩阵法可以利用矩阵乘幂的便利, 把复杂度降低为 O(log(n)), 比迭代更快

可以说改进算法能提升一亿倍以上的速度...

#54楼 @luikore Erlang跑分就是比Ruby 2.0好... Ruby 2.0比JRuby好

Erlang跑分都和PHP不相上下了

http://benchmarksgame.alioth.debian.org/u64q/benchmark.php?test=all&lang=erlang&lang2=yarv&data=u64q

#27楼 @luikore

想起来了,Erlang还可以编译的。编译之后速度快了不少嘛。

1> c(fib).
{ok,fib}
2> fib:test().
730082:14930352
ok
3> c(fib,[native]).
{ok,fib}
4> fib:test().
272043:14930352
ok
5> 

看到PyPy的结果我就安心了 (2014的MacBook 主频2.6, 我想放到i74790上跑得更快) 也许Ruby该出个解释器叫RubyRuby

mic@mbp: ~/work/$ ruby2.0 foo.rb 
       user     system      total        real
 14930352
  2.140000   0.010000   2.150000 (  2.146835)

mic@mbp: ~/work/$ pypy bar.py 
14930352
0.649578094482

mic@mbp: ~/work/$ python3.4 bar.py 
14930352
3.95859599113
if n < 2
  1

Fibonacci sequence

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2, if n>1
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册