【问题标题】:Numpy individual element access slower than for listsNumpy 单个元素的访问速度比列表慢
【发布时间】:2015-05-30 15:49:36
【问题描述】:

我刚开始使用 Numpy 并注意到遍历 Numpy 数组中的每个元素比使用列表列表慢约 4 倍。我现在知道这违背了 Numpy 的目的,如果可能的话,我应该对函数进行矢量化。我的问题是为什么它慢了 4 倍。这似乎是一笔不小的数目。

我使用%timeit 运行了以下测试

import numpy as np
b = np.eye(1000)
a = b.tolist()

%timeit b[100][100] #1000000 loops, best of 3: 692 ns per loop
%timeit a[100][100] #10000000 loops, best of 3: 70.7 ns per loop
%timeit b[100,100] #1000000 loops, best of 3: 343 ns per loop
%timeit b.item(100,100) #1000000 loops, best of 3: 297 ns per loop

我尝试使用dis.dis 来查看幕后发生的事情,但得到了:

TypeError: don't know how to disassemble method-wrapper objects

然后我尝试查看 Numpy 源代码,但无法确定哪个文件对应于数组元素访问。我很好奇是什么导致了额外的开销,更重要的是如何在未来为自己解决这个问题。似乎 python 无法轻松编译为 C 代码,因此我可以看到其中的区别。但是有没有办法查看每行生成的字节码,以了解差异?

【问题讨论】:

  • 有趣的发现。从未见过 numpy 与列表的访问时间的实际比较。无论如何,对单个元素的 numpy 访问很慢。可能是由于引擎盖下的函数调用。如果您将函数调用添加到列表访问,则时间将关闭。类似 def funca(): return a[100][100]
  • 在其 C 版本中,Python 列表基本上是一个 PyObject 指针数组。要迭代它,你增加一个指针,取消引用它,你就完成了,你已经有了一个完全限定的 Python 对象。 NumPy 数组是基本类型的数组,在您的示例中可能是 C int。以int 形式获取值与从列表中获取 Python 对象一样快。但是您必须将其转换为 Python 对象,可能需要调用 PyLong_FromLong 或类似的东西,因此会产生额外的开销。
  • @Jaime 为什么不发布这个答案?我敢肯定这会半定期出现
  • @Jaime 您是说将其转换为 NumPy 标量类型而不是 Python 对象吗?

标签: python arrays list numpy


【解决方案1】:

当 numpy 从数组中的一个位置返回项目时,它必须将内部 C 类型(float、double 等)值转换为 Python 类型的标量值(int、long、float)。然后它返回对 Python 类型值的引用。此转换需要一些时间。

有趣的是,同样的低效率也会以另一种方式损害性能。我有一个 Python 列表,我正在使用来自 numpy 数组的索引值对其进行索引。发生相同的转换以创建索引到 Python 列表所需的 Python 整数值。我不得不用原生 Python 整数的中间数组重写我的算法。

【讨论】:

    【解决方案2】:

    总而言之:从 NumPy 数组中获取项目需要创建新的 Python 对象,而列表并非如此。此外,对于 NumPy 数组,索引比列表稍微复杂一些,这可能会增加一些额外的开销。


    回顾一下,您列出的 NumPy 操作执行以下操作:

    1. b[100][100]b 的第 100 行作为数组返回,然后获取该行索引 100 处的值,将值作为对象返回(例如 np.int64 类型)。
    2. b[100,100]直接返回第100行第100列的值(不先返回中间数组)。
    3. b.item(100,100) 与上面的 b[100,100] 相同,只是将值转换为原生 Python 类型并返回。

    在这些操作中,(1) 是最慢的,因为它需要两个连续的 NumPy 索引操作(我将在下面解释为什么这比列表索引慢)。 (2) 是最快的,因为只执行一个索引操作。操作 (3) 可能较慢,因为它是一个方法调用(这些在 Python 中通常很慢)。

    为什么 list 访问仍然比b[100,100] 快?

    对象创建

    Python 列表是指向内存中对象的指针数组。例如,[1, 2, 3] 列表不直接包含这些整数,而是指向内存地址的指针,每个整数对象都已经存在。为了从列表中获取项目,Python 只返回对该对象的引用。

    NumPy 数组不是对象的集合。数组np.array([1, 2, 3]) 只是一个连续的内存块,其中的位设置为表示这些整数值。要从此数组中获取整数,必须在与数组分开的内存中构造一个新的 Python 对象。例如,索引操作可能会返回np.int64 的对象:此对象以前不存在,必须创建。

    索引复杂度

    a[100][100](从列表中获取)比b[100,100](从数组中获取)更快的另外两个原因是:

    • 字节码操作码 BINARY_SUBSCR 在索引列表和数组时执行,但它针对 Python 列表进行了优化。

    • 处理 Python 列表整数索引的内部 C 函数非常简短。另一方面,NumPy 索引要复杂得多,需要执行大量代码来确定所使用的索引类型,以便返回正确的值。

    下面将更详细地描述使用a[100][100]b[100,100] 访问列表和数组中的元素的步骤。

    字节码

    列表和数组都触发了相同的四个字节码操作码:

      0 LOAD_NAME                0 (a)           # the list or array
      3 LOAD_CONST               0 (100)         # index number (tuple for b[100,100])
      6 BINARY_SUBSCR                            # find correct "getitem" function
      7 RETURN_VALUE                             # value returned from list or array
    

    注意:如果您开始对多维列表进行链式索引,例如a[100][100][100],你开始重复这些字节码指令。使用元组索引的 NumPy 数组不会发生这种情况:b[100,100,100] 仅使用四个指令。这就是为什么随着维度数量的增加,时序差距开始缩小的原因。

    找到正确的“getitem”函数

    访问列表和数组的函数不同,需要在每种情况下找到正确的函数。此任务由BINARY_SUBSCR 操作码处理:

    w = POP();                                            // our index
    v = TOP();                                            // our list or NumPy array
    if (PyList_CheckExact(v) && PyInt_CheckExact(w)) {    // do we have a list and an int?
        /* INLINE: list[int] */
        Py_ssize_t i = PyInt_AsSsize_t(w);
            if (i < 0)
                 i += PyList_GET_SIZE(v);
            if (i >= 0 && i < PyList_GET_SIZE(v)) {
                 x = PyList_GET_ITEM(v, i);               // call "getitem" for lists
                 Py_INCREF(x);
            }
            else
                goto slow_get;
         }
         else
           slow_get:
             x = PyObject_GetItem(v, w);                  // else, call another function
                                                          // to work out what is needed
         Py_DECREF(v);
         Py_DECREF(w);
         SET_TOP(x);
         if (x != NULL) continue;
         break;
    

    此代码针对 Python 列表进行了优化。如果函数看到一个列表,它会快速调用函数PyList_GET_ITEM。现在可以在所需的索引处访问此列表(请参阅下面的下一部分)。

    但是,如果它没有看到列表(例如,我们有一个 NumPy 数组),它会采用“slow_get”路径。这又会调用另一个函数PyObject_GetItem 来检查对象映射到哪个“getitem”函数:

    PyObject_GetItem(PyObject *o, PyObject *key)
    {
        PyMappingMethods *m;
    
        if (o == NULL || key == NULL)
            return null_error();
    
        m = o->ob_type->tp_as_mapping;
        if (m && m->mp_subscript)
            return m->mp_subscript(o, key);
        ...
    

    对于 NumPy 数组,正确的函数位于 mp_subscript 结构中的 PyMappingMethods 中。

    在调用这个正确的“get”函数之前,请注意额外的函数调用。这些调用增加了b[100] 的开销,但多少取决于 Python/NumPy 的编译方式、系统架构等。

    从 Python 列表中获取

    上面可以看到函数PyList_GET_ITEM被调用了。这是一个简短的函数,基本上看起来像这样*:

    PyList_GetItem(PyObject *op, Py_ssize_t i)
    {
        if (!PyList_Check(op)) {                            // check if list
            PyErr_BadInternalCall();
            return NULL;
        }
        if (i < 0 || i >= Py_SIZE(op)) {                    // check i is in range
            if (indexerr == NULL) {
                indexerr = PyUnicode_FromString(
                    "list index out of range");
                if (indexerr == NULL)
                    return NULL;
            }
            PyErr_SetObject(PyExc_IndexError, indexerr);
            return NULL;
        }
        return ((PyListObject *)op) -> ob_item[i];           // return reference to object
    }
    

    * PyList_GET_ITEM 实际上是这个函数的宏形式,它做同样的事情,减去错误检查。

    这意味着在 Python 列表的索引 i 处获取项目相对简单。在内部,Python 检查项目的类型是否为列表,i 是否在列表的正确范围内,然后返回对列表中对象的引用。

    从 NumPy 数组中获取

    相比之下,NumPy 必须做更多的工作才能返回所请求索引处的值。

    可以通过多种不同的方式对数组进行索引,而 NumPy 必须决定需要哪个索引例程。各种索引例程主要由mapping.c 中的代码处理。

    用于索引 NumPy 数组的任何内容都通过函数prepare_index 开始解析索引并存储有关广播、维数等信息。这是函数的调用签名:

    NPY_NO_EXPORT int
    prepare_index(PyArrayObject *self, PyObject *index,
                  npy_index_info *indices,
                  int *num, int *ndim, int *out_fancy_ndim, int allow_boolean)
    
     /* @param the array being indexed
      * @param the index object
      * @param index info struct being filled (size of NPY_MAXDIMS * 2 + 1)
      * @param number of indices found
      * @param dimension of the indexing result
      * @param dimension of the fancy/advanced indices part
      * @param whether to allow the boolean special case 
      */
    

    该函数必须进行大量检查。即使对于像b[100,100] 这样相对简单的索引,也必须推断出很多信息,以便 NumPy 可以将引用(视图)返回到正确的值。

    总之,找到 NumPy 的“getitem”函数需要更长的时间,而且处理数组索引的函数必然比 Python 列表的单个函数更复杂。

    【讨论】:

    • 您如何确定哪些字节码操作码被触发以进行列表访问?
    • @emschorsch:我用dis.dis('a[100]')在这里找到字节码。
    • 当我尝试a = [0]*1000; dis'dis('a[100]') 我得到:File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/dis.py", line 166, in findlabels oparg = ord(code[i]) + ord(code[i+1])*256 IndexError: string index out of range
    • @emschorsch:在 Python 3.4 中,您可以将一些字符串传递给 dis。在 Python 2 中,您可以编写一个函数并传递它,例如def get(): return a[100] 然后dis.dis(get)
    猜你喜欢
    • 2016-05-03
    • 1970-01-01
    • 2013-03-23
    • 2021-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-25
    相关资源
    最近更新 更多