【问题标题】:Why are Python's arrays slow?为什么 Python 的数组很慢?
【发布时间】:2016-08-15 04:25:05
【问题描述】:

我希望 array.array 比列表更快,因为数组似乎没有装箱。

但是,我得到以下结果:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

造成这种差异的原因是什么?

【问题讨论】:

  • numpy 工具可以有效地利用您的数组:%timeit np.sum(A):100 个循环,最好的 3 个:每个循环 8.87 毫秒
  • 我从来没有遇到过需要使用array 包的情况。如果你想做大量的数学运算,Numpy 以光速(即 C)运行,并且通常比诸如 sum() 之类的幼稚实现更好。
  • 密切的选民:为什么这个是基于意见的? OP 似乎在就可测量和可重复的现象提出具体的技术问题。
  • @NickT 阅读An optimization anecdote。事实证明,array 在将整数字符串(表示 ASCII 字节)转换为 str 对象方面非常快。 Guido 本人是在经过许多其他解决方案后才提出的,并且对性能感到非常惊讶。无论如何,这是我记得看到它有用的唯一地方。 numpy 更适合处理数组,但它是第 3 方依赖项。

标签: python arrays performance boxing python-internals


【解决方案1】:

storage 是“未装箱”的,但每次您访问一个元素时,Python 都必须将其“装箱”(将其嵌入到常规 Python 对象中)才能对其进行任何操作。例如,您的 sum(A) 遍历数组,并在常规 Python int 对象中一次一个地将每个整数装箱。这需要时间。在您的sum(L) 中,所有装箱都是在创建列表时完成的。

因此,最终,数组通常会更慢,但需要的内存要少得多。


这是来自 Python 3 最新版本的相关代码,但相同的基本思想适用于自 Python 首次发布以来的所有 CPython 实现。

这是访问列表项的代码:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

它几乎没有:somelist[i] 只返回列表中的第 i'th 个对象(并且 CPython 中的所有 Python 对象都是指向其初始段符合 struct PyObject 布局的结构的指针)。

这是array__getitem__ 实现,类型代码为l

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

原始内存被视为平台原生Clong整数的向量; i'th C long 已阅读;然后调用PyLong_FromLong() 将本机C long 包装(“框”)在Python long 对象中(在Python 3 中,它消除了Python 2 在intlong 之间的区别,实际上显示了作为类型int)。

这种装箱必须为 Python int 对象分配新内存,并将本机 C long 的位喷入其中。在原始示例的上下文中,该对象的生命周期非常短暂(刚好足够sum() 将内容添加到运行总数中),然后需要更多时间来释放新的int 对象。

这就是速度差异的来源,在 CPython 实现中始终来自,并且将永远来自。

【讨论】:

【解决方案2】:

为了补充 Tim Peters 的出色答案,数组实现了 buffer protocol,而列表没有。这意味着,如果您正在编写 C 扩展(或道德上的等价物,例如编写 Cython 模块),那么您可以比任何方式更快地访问和处理数组的元素Python可以做到。这将为您带来相当大的速度提升,可能远远超过一个数量级。但是,它有许多缺点:

  1. 您现在正在编写 C 而不是 Python。 Cython 是改善这种情况的一种方法,但它并不能消除语言之间的许多根本差异;您需要熟悉 C 语义并了解它在做什么。
  2. PyPy 的 C API 工作 to some extent,但不是很快。如果您的目标是 PyPy,您可能应该只使用常规列表编写简单的代码,然后让 JITter 为您优化它。
  3. C 扩展比纯 Python 代码更难分发,因为它们需要编译。编译往往依赖于架构和操作系统,因此您需要确保针对您的目标平台进行编译。

根据您的用例,直接使用 C 扩展可能会使用大锤来打苍蝇。你应该首先调查NumPy,看看它是否足够强大,可以做任何你想做的数学。如果使用得当,它也会比原生 Python 快得多。

【讨论】:

    【解决方案3】:

    Tim Peters 回答了为什么这很慢,但让我们看看如何改进

    坚持您的 sum(range(...)) 示例(比您的示例小 10 倍以适合此处的内存):

    import numpy
    import array
    L = list(range(10**7))
    A = array.array('l', L)
    N = numpy.array(L)
    
    %timeit sum(L)
    10 loops, best of 3: 101 ms per loop
    
    %timeit sum(A)
    1 loop, best of 3: 237 ms per loop
    
    %timeit sum(N)
    1 loop, best of 3: 743 ms per loop
    

    这种方式 numpy 也需要装箱/拆箱,这有额外的开销。为了使其更快,必须保持在 numpy c 代码中:

    %timeit N.sum()
    100 loops, best of 3: 6.27 ms per loop
    

    所以从列表解决方案到 numpy 版本,这是运行时的 16 倍。

    我们还要检查创建这些数据结构需要多长时间

    %timeit list(range(10**7))
    1 loop, best of 3: 283 ms per loop
    
    %timeit array.array('l', range(10**7))
    1 loop, best of 3: 884 ms per loop
    
    %timeit numpy.array(range(10**7))
    1 loop, best of 3: 1.49 s per loop
    
    %timeit numpy.arange(10**7)
    10 loops, best of 3: 21.7 ms per loop
    

    明显的赢家:Numpy

    还要注意,创建数据结构所花费的时间与求和所花费的时间差不多,甚至更多。分配内存很慢。

    这些的内存使用情况:

    sys.getsizeof(L)
    90000112
    sys.getsizeof(A)
    81940352
    sys.getsizeof(N)
    80000096
    

    因此,每个数字占用 8 个字节,开销不同。对于我们使用 32 位整数的范围就足够了,所以我们可以保护一些内存。

    N=numpy.arange(10**7, dtype=numpy.int32)
    
    sys.getsizeof(N)
    40000096
    
    %timeit N.sum()
    100 loops, best of 3: 8.35 ms per loop
    

    但事实证明,在我的机器上添加 64 位整数比 32 位整数要快,所以只有在内存/带宽有限的情况下才值得这样做。

    【讨论】:

      【解决方案4】:

      请注意100000000等于10^8而不是10^7,我的结果如下:

      100000000 == 10**8
      
      # my test results on a Linux virtual machine:
      #<L = list(range(100000000))> Time: 0:00:03.263585
      #<A = array.array('l', range(100000000))> Time: 0:00:16.728709
      #<L = list(range(10**8))> Time: 0:00:03.119379
      #<A = array.array('l', range(10**8))> Time: 0:00:18.042187
      #<A = array.array('l', L)> Time: 0:00:07.524478
      #<sum(L)> Time: 0:00:01.640671
      #<np.sum(L)> Time: 0:00:20.762153
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-10-02
        • 2010-10-30
        • 2023-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-10-10
        相关资源
        最近更新 更多