与“普通”python 相比,Cython 让我们可以更多地访问 array.array 的内部结构,因此我们可以利用它来加速代码:
- 对于您的小示例,几乎是因子
7(通过消除大部分开销)。
- 因子
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!