Cython比“普通”的Python更能够访问array.array的内部,因此我们可以利用它来加速代码:
- 对于您的小示例,通过消除大部分开销,几乎可以提高7倍。
- 通过消除不必要的数组复制,可以将较大输入的速度提高2倍。
继续阅读以获取更多详细信息。
“这样为小输入优化函数有点不寻常,但并非没有(至少在理论上)的意义。”
“因此,让我们以您的函数作为基准线开始:”
a=array('l', [1,2,3])
1.03 µs ± 10.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
lst=[1,2,3]
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))
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)
Cython 版本对于
list
大约快了 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
添加元素:通常情况下,
它会过度分配,因此
append()
的摊销成本为
O(1)
,但是在
array.copy
之后,新数组恰好包含所需数量的元素,并且下一个
append
将调用重新分配。我们需要改变这一点(有关使用的函数的说明,请参见
此处):
%%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)
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
pyappend2 1.02 µs
cyappend 0.94 µs
cyappend2 0.90 µs
cyappend3 0.43 µs
pylistappend 4.09 µs
cylistappend 3.85 µs
现在,消除
array.array
的不必要复制,可以得到我们期望的因子2。
对于更大的数组(
10000
个元素),我们看到如下内容:
pyappend 6.9 µs
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!
array.array
数组?不清楚你为什么需要它们。 - user2357112array.array
作为类型强制数组。我也可以使用np.array。问题是一样的。我不确定这是否是最快的纯C实现,没有Python类型检查的开销。另外,假设我将变量定义为“long”。 - Weiwen Gulist
是最快的选项的原因是它被设计为可调整大小,因此分配比所需空间更多的空间以便有足够的空间进行扩展。如果你想要快速的东西,那么你应该做同样的事情。 - DavidW