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

jjym · 2013年07月22日 · 最后由 reboot 回复于 2016年03月25日 · 19160 次阅读
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 代码写的有问题?

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 了,这怎么解释。

#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
需要 登录 后方可回复, 如果你还没有账号请 注册新账号