【问题标题】:What is the fastest way in Cython to create a new array from an existing array and a variableCython中从现有数组和变量创建新数组的最快方法是什么
【发布时间】:2017-10-29 04:13:20
【问题描述】:

假设我有一个数组

from array import array
myarr = array('l', [1, 2, 3])

和一个变量: myvar = 4 创建新数组的最快方法是什么:

newarray = array('l', [1, 2, 3, 4])

您可以假设所有元素都是“长”类型

我尝试创建一个新数组并使用array.append() 不确定它是否最快。我正在考虑使用memoryview,例如: malloc(4*sizeof(long)) 但我不知道如何将较短的数组复制到内存视图的一部分中。然后将最后一个元素插入到最后一个位置。

我对 Cython 还很陌生。感谢您的帮助!

更新: 我比较了以下三种方法:

Cython: [100000 次循环,最好的 3 次:每个循环 5.94 µs]

from libc.stdlib cimport malloc

def cappend(long[:] arr, long var, size_t N):
    cdef long[:] result = <long[:(N+1)]>malloc((N+1)*sizeof(long))
    result.base[:N] = arr
    result.base[N] = var
    return result

数组: [1000000 次循环,3 次中的最佳:每个循环 1.21 µs]

from array import array
import copy
def pyappend(arr, x):
    result = copy.copy(arr)
    result.append(x)
    return result

列表追加: [1000000 次循环,最好的 3:每个循环 480 ns]

def pylistappend(lst, x):
    result = lst[:]
    result.append(x)
    return result

有没有希望改进cython部分并击败数组一?

【问题讨论】:

  • 你实际上是在使用 Cython - 就像在 this thing 中一样 - 还是你把它和其他东西混在一起了?另外,你为什么选择array.array 数组?目前尚不清楚您为什么需要它们。
  • 使用docs,可能只是克隆和追加。
  • 不过,您可能希望将显式类型放在变量上。
  • @user2357112 是的,我正在使用 cython。我将array.array 用于类型强制数组。我也可以使用 np.array 。问题是一样的。我不确定这在纯 C 实现的意义上是最快的,没有开销 python 类型检查。是的,假设我将变量定义为long
  • 这是你不应该经常做的事情——它永远不会很快,因为它通常必须复制旧数组中的所有数据。 list 是最快选项的原因是它被设计为可以调整大小,因此分配的空间比它需要的空间要多。如果你想要快速的东西,那么你也应该这样做。

标签: python arrays cython cpython


【解决方案1】:

与“普通”python 相比,Cython 让我们可以更多地访问 array.array 的内部结构,因此我们可以利用它来加速代码:

  1. 对于您的小示例,几乎是因子 7(通过消除大部分开销)。
  2. 因子2 通过消除不必要的数组复制来获得更大的输入。

继续阅读以了解更多详情。


尝试针对如此小的输入优化函数有点不寻常,但并非没有(至少理论上的)兴趣。

让我们从你的函数作为基线开始:

a=array('l', [1,2,3])
%timeit pyappend(a, 8)
1.03 µs ± 10.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

lst=[1,2,3]
%timeit pylistappend(lst, 8)
279 ns ± 6.03 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

我们必须知道:我们测量的不是复制成本,而是开销成本(python解释器,调用函数等),例如a有3个或5个元素没有区别:

a=array('l', range(5))
%timeit pyappend(a, 8)
1.03 µs ± 6.76 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

在数组版本中,我们有更多的开销,因为我们通过copy 模块进行间接处理,我们可以尝试消除它:

def pyappend2(arr, x): 
      result = array('l',arr)                   
      result.append(x)                               
      return result

%timeit pyappend2(a, 8)
496 ns ± 5.04 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

这样更快。现在让我们使用 cython - 这将消除解释器的成本:

 %%cython
 def cylistappend(lst, x):      
     result = lst[:]                                  
     result.append(x)                            
     return result

 %%cython
 from cpython cimport array
 def cyappend(array.array arr, long long int x):
      cdef array.array res = array.array('l', arr)
      res.append(x)
      return res   

 %timeit cylistappend(lst, 8)
 193 ns ± 12.4 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
 %%timeit cyappend(a, 8)
 421 ns ± 8.08 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

list 的 cython 版本快了大约 33%,array 快了大约 10%。构造函数array.array() 需要一个可迭代对象,但我们已经有一个array.array,因此我们使用cpython 中的功能来访问array.array 对象的内部并稍微改善这种情况:

 %%cython
 from cpython cimport array
 def cyappend2(array.array arr, long long int x):
     cdef array.array res = array.copy(arr)
     res.append(x)
     return res

 %timeit cyappend2(a, 8)
 305 ns ± 7.25 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

下一步我们需要知道array.array如何附加元素:通常it over-allocates,所以append()具有摊销成本O(1),但是在array.copy之后,新数组正是所需的元素数量下一个append 调用重新分配。我们需要改变它(参见here 对所用函数的描述):

  %%cython
  from cpython cimport array
  from libc.string cimport memcpy
  def cyappend3(array.array arr, long long int x):
           cdef Py_ssize_t n=len(arr)
           cdef array.array res = array.clone(arr,n+1,False)
           memcpy(res.data.as_voidptr, arr.data.as_voidptr, 8*n)#that is pretty sloppy..
           res.data.as_longlongs[n]=x
           return res

 %timeit cyappend3(a, 8)
 154 ns ± 1.34 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

与你的函数类似,内存被过度分配,所以我们不再需要调用resize()。现在我们比list 快,比原来的python 版本快了近7 倍。


让我们比较更大的数组大小(a=array('l',range(1000))lst=list(range(1000)),其中数据的复制占用了大部分运行时间:

  pyappend             1.84 µs  #copy-module is slow!
  pyappend2            1.02 µs
  cyappend             0.94 µs  #cython no big help - we are copying twice
  cyappend2            0.90 µs  #still copying twice
  cyappend3            0.43 µs  #copying only once -> twice as fast!

  pylistappend         4.09 µs # needs to increment refs of integers
  cylistappend         3.85 µs # the same as above

现在,消除array.array 的不必要副本给了我们预期的因子 2。


对于更大的数组(10000 元素),我们看到以下内容:

  pyappend             6.9 µs  #copy-module is slow!
  pyappend2            4.8 µs
  cyappend2            4.4 µs  
  cyappend3            4.4 µs  

版本之间不再存在差异(如果放弃慢速复制模块)。其原因是 array.array 对如此大量的元素的行为发生了变化:复制时过度分配,从而避免了在第一个 append() 之后的重新分配。

我们可以很容易地检查它:

b=array('l', array('l', range(10**3)))#emulate our functions
b.buffer_info()
[] (94481422849232, 1000)
b.append(1)
b.buffer_info()
[] (94481422860352, 1001) # another pointer address -> reallocated
...
b=array('l', array('l', range(10**4)))
b.buffer_info()
[](94481426290064, 10000)
b.append(33)
b.buffer_info()
[](94481426290064, 10001) # the same pointer address -> no reallocation!

【讨论】:

  • 很好的答案和详细的分析!非常感谢。
猜你喜欢
  • 2015-11-26
  • 1970-01-01
  • 2017-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多