【问题标题】:Efficient ways to duplicate array/list in Python在 Python 中复制数组/列表的有效方法
【发布时间】:2012-10-14 09:57:52
【问题描述】:

注意:我是一名 Ruby 开发人员,试图在 Python 中找到自己的方式。

当我想弄清楚为什么某些脚本使用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:mylist[:] 的不同之处在于它比 mylist[0:len(mylist)]25 %。它是直接复制到内存中还是什么?

问题 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

【问题讨论】:

  • 使用 python timeit module 来测量 python 执行时间。我怀疑它会让事情(很多)更快,但它会避免所有常见的时间陷阱。
  • alist[:]alist[0:len(alist)]的时差;后者创建 python int 对象,前一种方法不需要处理。
  • @MartijnPieters -- 后者每次还需要查找全局len(并调用)
  • Array(a) 不复制数组。当给定一个数组时,它只调用to_ary,它返回self。您还应该使用Ruby's Benchmark library 而不是手动计时。
  • 在 Ruby 中尝试 obj.dup 并进行基准测试。

标签: python ruby arrays


【解决方案1】:

在 python 中使用timeit 模块来测试时间。

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)] 中涉及的额外步骤是它缓慢的原因。

这是两者的字节码比较:

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        

【讨论】:

  • 这肯定回答了我的问题,并表明 n00b 可以通过多种方式编写低效代码 - 即使在后一种 timeit 变体中,我的 copy 也比您的实现慢得多。谢谢! ;-)
  • @Laas 很高兴这有帮助 :),其中哪一个是您系统中最快的?
  • @Laas 你是对的 copy() is not the fastest one, I gad a mistake in my code(forgot to call cop` 函数及时)
  • 是的,我也获得了a[:] 作为获胜者。我用我的时间更新了问题。
【解决方案2】:

我无法评论 ruby​​ 时序与 python 时序。但我可以评论listslice。下面是对字节码的快速检查:

>>> 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 需要LOAD_GLOBAL 才能找到函数list。在 python 中查找全局变量(和调用函数)相对较慢。这可以解释为什么a[0:len(a)] 也更慢。还要记住list 需要能够处理任意迭代器,而切片则不需要。这意味着list 需要分配一个新列表,在迭代列表时将元素打包到该列表中,并在必要时调整大小。这里有一些昂贵的东西——必要时调整大小和迭代(在 python 中有效,而不是在 C 中)。使用切片方法,您可以计算出您需要的内存大小,这样可能可以避免调整大小,并且迭代可以完全在 C 中完成(可能使用memcpy 或其他东西。

免责声明:我不是 python 开发者,所以我不确定list() 的内部是如何实现的。我只是根据我对规范的了解进行推测。

编辑 -- 所以我查看了源代码(在 Martijn 的指导下)。相关代码在listobject.clist 调用 list_init 然后在第 799 行调用 listextend。如果对象是列表或元组(第 812 行),该函数会检查它是否可以使用快速分支。最后,从第 834 行开始完成繁重的工作:

 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

【讨论】:

  • 很奇怪,但list(a) 是我系统上最快的。
  • @AshwiniChaudhary -- 什么系统?这很奇怪。在我的 OS-X 系统和 Ubuntu linux 系统上,它对我来说是最慢的。
  • @AshwiniChaudhary:您的查找是本地的,而在 mgilson 的情况下是全局的。如果 mgilson 在他的函数中添加len = __builtins__.len 也会更快,我敢打赌。
  • @MartijnPieters -- Ashwini 的本地查找如何?仅仅因为a 是本地的,这不会使len 成为本地...(尽管您是对的,查找局部变量的速度比全局变量快...)。我怀疑他测试的问题是range 包含在计时中。这可能会使他的结果对各种系统波动更加敏感......
  • @mgilson:它在 iPython shell 中,所以 locals() is globals()True。并且range() 不包含在他的计时中,仅包含在反汇编示例中。
猜你喜欢
  • 1970-01-01
  • 2012-02-21
  • 1970-01-01
  • 2015-08-14
  • 1970-01-01
  • 2011-01-10
  • 1970-01-01
  • 2021-12-24
  • 2013-01-21
相关资源
最近更新 更多