9

注:私はPythonで自分の道を見つけようとしているRuby開発者です。

一部のスクリプトがリストを複製するmylist[:]代わりに使用する理由を理解したいときは、複製するさまざまな方法の簡単なベンチマークを作成しました(以下のコードを参照)。list(mylist)range(10)

編集:以下に提案するように、Pythonを利用するようにテストを更新しましたtimeit。これにより、Rubyと直接比較することができなくなります。timeitはRubyの場合とは異なり、ループを考慮しないBenchmarkため、Rubyコードは参照用です。

Python 2.7.2

Array duplicating. Tests run 50000000 times
list(a)     18.7599430084
copy(a)     59.1787488461
a[:]         9.58828091621
a[0:len(a)] 14.9832749367

参考までに、Rubyでも同じスクリプトを作成しました。

Ruby 1.9.2p0

Array duplicating. Tests 50000000 times
                      user     system      total        real
Array.new(a)     14.590000   0.030000  14.620000 ( 14.693033)
Array[*a]        18.840000   0.060000  18.900000 ( 19.156352)
a.take(a.size)    8.780000   0.020000   8.800000 (  8.805700)
a.clone          16.310000   0.040000  16.350000 ( 16.384711)
a[0,a.size]       8.950000   0.020000   8.970000 (  8.990514)

質問1:それが25%速いということで何がmylist[:]違うのか。メモリに直接コピーしますか、それとも何ですか?mylist[0:len(mylist)]

質問2: 編集:更新されたベンチマークでは、PythonとRubyで大きな違いは見られなくなりました。だった: RubyコードがPythonよりもはるかに高速になるように、明らかに非効率的な方法でテストを実装しましたか?

今コードリスト:

Python:

import timeit

COUNT = 50000000

print "Array duplicating. Tests run", COUNT, "times"

setup = 'a = range(10); import copy'

print "list(a)\t\t", timeit.timeit(stmt='list(a)', setup=setup, number=COUNT)
print "copy(a)\t\t", timeit.timeit(stmt='copy.copy(a)', setup=setup, number=COUNT)
print "a[:]\t\t", timeit.timeit(stmt='a[:]', setup=setup, number=COUNT)
print "a[0:len(a)]\t", timeit.timeit(stmt='a[0:len(a)]', setup=setup, number=COUNT)

ルビー:

require 'benchmark'

a = (0...10).to_a

COUNT = 50_000_000

puts "Array duplicating. Tests #{COUNT} times"

Benchmark.bm(16) do |x|
  x.report("Array.new(a)")   {COUNT.times{ Array.new(a) }}
  x.report("Array[*a]")   {COUNT.times{ Array[*a] }}
  x.report("a.take(a.size)")   {COUNT.times{ a.take(a.size) }}
  x.report("a.clone")    {COUNT.times{ a.clone }}
  x.report("a[0,a.size]"){COUNT.times{ a[0,a.size] }}
end
4

2 に答える 2

9

timeitタイミングをテストするには、Pythonのモジュールを使用します。

from copy import *

a=range(1000)

def cop():
    b=copy(a)

def func1():
    b=list(a)

def slice():
    b=a[:]

def slice_len():
    b=a[0:len(a)]



if __name__=="__main__":
    import timeit
    print "copy(a)",timeit.timeit("cop()", setup="from __main__ import cop")
    print "list(a)",timeit.timeit("func1()", setup="from __main__ import func1")
    print "a[:]",timeit.timeit("slice()", setup="from __main__ import slice")
    print "a[0:len(a)]",timeit.timeit("slice_len()", setup="from __main__ import slice_len")

結果:

copy(a) 3.98940896988
list(a) 2.54542589188
a[:] 1.96630120277                   #winner
a[0:len(a)] 10.5431251526

それは確かに含まれる余分なステップa[0:len(a)]がそれが遅い理由です。

2つのバイトコードの比較は次のとおりです。

In [19]: dis.dis(func1)
  2           0 LOAD_GLOBAL              0 (range)
              3 LOAD_CONST               1 (100000)
              6 CALL_FUNCTION            1
              9 STORE_FAST               0 (a)

  3          12 LOAD_FAST                0 (a)
             15 SLICE+0             
             16 STORE_FAST               1 (b)
             19 LOAD_CONST               0 (None)
             22 RETURN_VALUE        

In [20]: dis.dis(func2)
  2           0 LOAD_GLOBAL              0 (range)
              3 LOAD_CONST               1 (100000)
              6 CALL_FUNCTION            1
              9 STORE_FAST               0 (a)

  3          12 LOAD_FAST                0 (a)    #same up to here
             15 LOAD_CONST               2 (0)    #loads 0
             18 LOAD_GLOBAL              1 (len) # loads the builtin len(),
                                                 # so it might take some lookup time
             21 LOAD_FAST                0 (a)
             24 CALL_FUNCTION            1         
             27 SLICE+3             
             28 STORE_FAST               1 (b)
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE        
于 2012-10-24T11:10:39.510 に答える
6

ルビーのタイミングとPythonのタイミングについてはコメントできません。listしかし、私は対についてコメントすることができsliceます。バイトコードの簡単な検査は次のとおりです。

>>> import dis
>>> a = range(10)
>>> def func(a):
...     return a[:]
... 
>>> def func2(a):
...     return list(a)
... 
>>> dis.dis(func)
  2           0 LOAD_FAST                0 (a)
              3 SLICE+0             
              4 RETURN_VALUE        
>>> dis.dis(func2)
  2           0 LOAD_GLOBAL              0 (list)
              3 LOAD_FAST                0 (a)
              6 CALL_FUNCTION            1
              9 RETURN_VALUE 

関数を見つけるにはlistが必要であることに注意してください。Pythonでグローバルを検索する(そして関数を呼び出す)のは比較的遅いです。これは、なぜ遅いのかを説明します。また、スライスはそうではないのに対し、任意のイテレータを処理できる必要があることを忘れないでください。つまり、新しいリストを割り当て、リストを繰り返し処理するときに要素をそのリストにパックし、必要に応じてサイズを変更する必要があります。ここには高価なものがいくつかあります-必要に応じてサイズを変更し、繰り返します(CではなくPythonで効果的に)。スライス方法を使用すると、必要なメモリのサイズを計算できるため、サイズ変更を回避でき、反復は完全にCで実行できます(おそらく、または何かを使用して)。LOAD_GLOBALlista[0:len(a)]listlistmemcpy

免責事項:私はPython開発者ではないので、の内部がどのようにlist()実装されているかはわかりません。私は仕様について知っていることに基づいて推測しているだけです。

編集 -それで私はソースを見ました(Martijnからの少しのガイダンスで)。関連するコードはにありlistobject.cます。 次に、行799で呼び出す呼び出しlist。この関数には、オブジェクトがリストまたはタプルの場合に高速分岐を使用できるかどうかを確認するためのチェックがいくつかあります(行812)。最後に、重量物の持ち上げは834行目から行われます。list_initlistextend

 src = PySequence_Fast_ITEMS(b);
 dest = self->ob_item + m;
 for (i = 0; i < n; i++) {
     PyObject *o = src[i];
     Py_INCREF(o);
     dest[i] = o;
 }

list_subscriptそれを(2544行目)で定義されていると思うスライスバージョンと比較してください。これはlist_slice(2570行目)を呼び出し、次のループ(486行目)によって重い持ち上げが行われます。

 src = a->ob_item + ilow;
 dest = np->ob_item;
 for (i = 0; i < len; i++) {
     PyObject *v = src[i];
     Py_INCREF(v);
     dest[i] = v;
 }

これらはほとんど同じコードなので、パフォーマンスが大きなリストでもほぼ同じであることは驚くことではありません(スライスの解凍、グローバル変数の検索などの小さなもののオーバーヘッドはそれほど重要ではなくなります)


Pythonテスト(およびUbuntuシステムの結果)を実行する方法は次のとおりです。

$ python -m timeit -s 'a=range(30)' 'list(a)'
1000000 loops, best of 3: 0.39 usec per loop
$ python -m timeit -s 'a=range(30)' 'a[:]'
10000000 loops, best of 3: 0.183 usec per loop
$ python -m timeit -s 'a=range(30)' 'a[0:len(a)]'
1000000 loops, best of 3: 0.254 usec per loop
于 2012-10-24T11:22:21.310 に答える