Cython从现有数组和变量

时间:2017-10-27 17:09:39

标签: python arrays cython cpython

假设我有一个数组

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

和变量: myvar = 4 什么是创建新阵列的最快方法:

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

您可以假设所有元素都是'long'类型

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

我是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部分并击败阵列吗?

1 个答案:

答案 0 :(得分:4)

Cython让我们更多地访问array.array的内部而不是&#34;正常&#34; python,所以我们可以利用它来加速代码:

  1. 几乎通过因子7为您的小例子(通过消除大部分开销)。
  2. 通过消除不必要的数组副本,
  3. 通过因子2获得更大的输入。
  4. 继续阅读以获取更多详情。

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

    因此,让我们以您的职能作为基线开始:

    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-allocatesappend()已分摊费用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!