我的知识仍然不够完美,但这可能会有所帮助。我进行了一些非正式的基准测试,展示了每种数组类型适用于哪些情况,并对我发现的结果感到好奇。
虽然这些数组类型在许多方面都不同,但如果您正在使用大型数组进行重计算,则应该能够从中获得类似的性能,因为逐个项目的访问应该在各方面大致相同。
NumPy数组是使用Python的C API实现的Python对象。NumPy数组确实提供了一个C级别的API,但它们不能独立于Python解释器创建。它们特别有用,因为NumPy和SciPy中提供了许多不同的数组操作例程。
Cython内存视图也是Python对象,但它是作为Cython扩展类型制作的。它似乎并不是为纯Python设计的,因为它不是Cython的一部分,无法直接从Python导入,但您可以从Cython函数返回视图。您可以查看
https://github.com/cython/cython/blob/master/Cython/Utility/MemoryView.pyx上的实现。
C数组是C语言中的本机类型。它像指针一样索引,但数组和指针是不同的。这里有一些很好的讨论:
http://c-faq.com/aryptr/index.html。它们可以在堆栈上分配,并且更容易被C编译器优化,但是在Cython外部访问它们将更加困难。我知道您可以从其他程序动态分配的内存中创建NumPy数组,但以这种方式似乎更加困难。Travis Oliphant在
http://blog.enthought.com/python/numpy-arrays-with-pre-allocated-memory/上发布了一个示例。如果您正在使用C数组或指针作为程序内的临时存储,则应该非常适合您。对于任何其他类型的切片或矢量化计算,它们将不太方便,因为您必须使用显式循环自己完成所有操作,但它们应该更快地分配和释放,并且应该提供良好的速度基线。
Cython还提供了一个数组类。它看起来是为内部使用而设计的。当复制内存视图时,将创建实例。请参见
http://docs.cython.org/src/userguide/memoryviews.html#view-cython-arrays。
在Cython中,您还可以分配内存并索引指针以将分配的内存视为数组。请参见
http://docs.cython.org/src/tutorial/memory_allocation.html。
这里有一些基准测试结果,显示了对于索引大数组的相似性能。这是Cython文件。
from numpy cimport ndarray as ar, uint64_t
cimport cython
import numpy as np
@cython.boundscheck(False)
@cython.wraparound(False)
def ndarr_time(uint64_t n=1000000, uint64_t size=10000):
cdef:
ar[uint64_t] A = np.empty(n, dtype=np.uint64)
uint64_t i, j
for i in range(n):
for j in range(size):
A[j] = n
def carr_time(uint64_t n=1000000):
cdef:
ar[uint64_t] A = np.empty(n, dtype=np.uint64)
uint64_t AC[10000]
uint64_t a
int i, j
for i in range(n):
for j in range(10000):
AC[j] = n
@cython.boundscheck(False)
@cython.wraparound(False)
def ptr_time(uint64_t n=1000000, uint64_t size=10000):
cdef:
ar[uint64_t] A = np.empty(n, dtype=np.uint64)
uint64_t* AP = &A[0]
uint64_t a
int i, j
for i in range(n):
for j in range(size):
AP[j] = n
@cython.boundscheck(False)
@cython.wraparound(False)
def view_time(uint64_t n=1000000, uint64_t size=10000):
cdef:
ar[uint64_t] A = np.empty(n, dtype=np.uint64)
uint64_t[:] AV = A
uint64_t i, j
for i in range(n):
for j in range(size):
AV[j] = n
使用IPython计时,我们得到了以下结果。
%timeit -n 10 ndarr_time()
%timeit -n 10 carr_time()
%timeit -n 10 ptr_time()
%timeit -n 10 view_time()
10 loops, best of 3: 6.33 s per loop
10 loops, best of 3: 3.12 s per loop
10 loops, best of 3: 6.26 s per loop
10 loops, best of 3: 3.74 s per loop
考虑到 Efficiency: arrays vs pointers 中指出数组不太可能比指针快,这些结果让我感到有点奇怪。看起来某种编译器优化使得纯C数组和类型化内存视图更快。我尝试关闭C编译器上的所有优化标志,并获得了时间记录。
1 loops, best of 3: 25.1 s per loop
1 loops, best of 3: 25.5 s per loop
1 loops, best of 3: 32 s per loop
1 loops, best of 3: 28.4 s per loop
在我看来,无论是哪种方式逐个访问元素,都差不多,只不过C数组和Cython内存视图更容易被编译器优化。
更多评论可以在我之前发现的这两篇博客文章中看到:
http://jakevdp.github.io/blog/2012/08/08/memoryview-benchmarks/
http://jakevdp.github.io/blog/2012/08/16/memoryview-benchmarks-2/
在第二篇博客文章中,作者提到如果内存视图切片被内联,则它们可以提供类似指针算术的速度。
在我的一些测试中,我注意到明确内联使用内存视图切片的函数并不总是必要的。
作为一个例子,我将计算数组中任意两行的内积。
from numpy cimport ndarray as ar
cimport cython
from numpy import empty
@cython.boundscheck(False)
@cython.wraparound(False)
cdef inline double dot_product(double[:] a, double[:] b, int size):
cdef int i
cdef double tot = 0.
for i in range(size):
tot += a[i] * b[i]
return tot
@cython.boundscheck(False)
@cython.wraparound(False)
cdef double dot_product_no_inline(double[:] a, double[:] b, int size):
cdef int i
cdef double tot = 0.
for i in range(size):
tot += a[i] * b[i]
return tot
@cython.boundscheck(False)
@cython.wraparound(False)
def dot_rows_slicing(ar[double,ndim=2] A):
cdef:
double[:,:] Aview = A
ar[double,ndim=2] res = empty((A.shape[0], A.shape[0]))
int i, j
for i in range(A.shape[0]):
for j in range(A.shape[0]):
res[i,j] = dot_product(Aview[i], Aview[j], A.shape[1])
return res
@cython.boundscheck(False)
@cython.wraparound(False)
def dot_rows_slicing_no_inline(ar[double,ndim=2] A):
cdef:
double[:,:] Aview = A
ar[double,ndim=2] res = empty((A.shape[0], A.shape[0]))
int i, j
for i in range(A.shape[0]):
for j in range(A.shape[0]):
res[i,j] = dot_product_no_inline(Aview[i], Aview[j], A.shape[1])
return res
@cython.boundscheck(False)
@cython.boundscheck(False)
cdef inline double ndarr_dot_product(ar[double] a, ar[double] b):
cdef int i
cdef double tot = 0.
for i in range(a.size):
tot += a[i] * b[i]
return tot
@cython.boundscheck(False)
@cython.boundscheck(False)
cdef double ndarr_dot_product_no_inline(ar[double] a, ar[double] b):
cdef int i
cdef double tot = 0.
for i in range(a.size):
tot += a[i] * b[i]
return tot
@cython.boundscheck(False)
@cython.wraparound(False)
def ndarr_dot_rows_slicing(ar[double,ndim=2] A):
cdef:
ar[double,ndim=2] res = empty((A.shape[0], A.shape[0]))
int i, j
for i in range(A.shape[0]):
for j in range(A.shape[0]):
res[i,j] = ndarr_dot_product(A[i], A[j])
return res
@cython.boundscheck(False)
@cython.wraparound(False)
def ndarr_dot_rows_slicing_no_inline(ar[double,ndim=2] A):
cdef:
ar[double,ndim=2] res = empty((A.shape[0], A.shape[0]))
int i, j
for i in range(A.shape[0]):
for j in range(A.shape[0]):
res[i,j] = ndarr_dot_product(A[i], A[j])
return res
@cython.boundscheck(False)
@cython.wraparound(False)
def dot_rows_loops(ar[double,ndim=2] A):
cdef:
ar[double,ndim=2] res = empty((A.shape[0], A.shape[0]))
int i, j, k
double tot
for i in range(A.shape[0]):
for j in range(A.shape[0]):
tot = 0.
for k in range(A.shape[1]):
tot += A[i,k] * A[j,k]
res[i,j] = tot
return res
计时这些操作,我们可以看到:
A = rand(1000, 1000)
%timeit dot_rows_slicing(A)
%timeit dot_rows_slicing_no_inline(A)
%timeit ndarr_dot_rows_slicing(A)
%timeit ndarr_dot_rows_slicing_no_inline(A)
%timeit dot_rows_loops(A)
1 loops, best of 3: 1.02 s per loop
1 loops, best of 3: 1.02 s per loop
1 loops, best of 3: 3.65 s per loop
1 loops, best of 3: 3.66 s per loop
1 loops, best of 3: 1.04 s per loop
结果在显式内联和没有内联的情况下都很快。
在两种情况下,经过类型化的内存视图与没有切片编写的函数版本相当。
在博客文章中,他不得不写一个特定的例子来强制编译器不内联一个函数。
看起来像一个体面的C编译器(我正在使用MinGW)能够处理这些优化而不需要告诉它内联某些函数。
即使没有显示内联,Memoryviews 在 Cython 模块内在函数之间传递数组切片时也可能更快。
然而,在这个特定的情况下,甚至将循环推到 C 中也无法达到通过正确使用矩阵乘法所能实现的速度。
BLAS 仍然是执行此类操作的最佳方法。
%timeit A.dot(A.T)
10 loops, best of 3: 25.7 ms per loop
还有一种自动将NumPy数组转换为memoryviews的方法,如下所示:
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cysum(double[:] A):
cdef tot = 0.
cdef int i
for i in range(A.size):
tot += A[i]
return tot
唯一的问题是,如果你想要一个函数返回一个NumPy数组,你需要使用
np.asarray
将内存视图对象转换回NumPy数组。
这是一个相对廉价的操作,因为内存视图符合
http://www.python.org/dev/peps/pep-3118/。
结论
对于Cython模块内部使用而言,类型化的内存视图似乎是NumPy数组的一个可行替代品。
使用内存视图进行数组切片会更快,但是与NumPy数组相比,内存视图没有那么多的函数和方法编写。
如果您不需要调用许多NumPy数组方法,并且想要轻松地进行数组切片,则可以使用内存视图来代替NumPy数组。
如果您需要给定数组的数组切片和NumPy功能,则可以创建指向与NumPy数组相同的内存的内存视图。
然后,您可以使用视图在函数之间传递切片,并使用数组调用NumPy函数。
这种方法仍然有些局限性,但如果您大部分处理都是使用单个数组完成的话,它将工作得很好。
C数组和/或动态分配的内存块可能对中间计算很有用,但是它们不容易传回Python以供使用。
我认为,动态分配多维C数组也更加麻烦。
我所知道的最好方法是分配一个大块的内存,然后使用整数算术将其索引为多维数组。
如果您想要轻松地动态分配数组,则可能会出现问题。
另一方面,对于C数组,分配时间可能快得多。
其他数组类型旨在几乎与C数组一样快且更加方便,因此除非有强制性原因,否则建议使用它们。
更新:如@Veedrac的答案中所提到的,您仍然可以将Cython内存视图传递给大多数NumPy函数。
当您这样做时,NumPy通常必须创建一个新的NumPy数组对象来使用内存视图,因此这会稍微慢一些。
对于大型数组,效果将是可以忽略不计的。
无论数组大小如何,调用
np.asarray
以获取内存视图将相对较快。
但是,为了证明这种效果,这里还有另一个基准测试:
def npy_call_on_view(npy_func, double[:] A, int n):
cdef int i
for i in range(n):
npy_func(A)
def npy_call_on_arr(npy_func, ar[double] A, int n):
cdef int i
for i in range(n):
npy_func(A)
在IPython中:
from numpy.random import rand
A = rand(1)
%timeit npy_call_on_view(np.amin, A, 10000)
%timeit npy_call_on_arr(np.amin, A, 10000)
输出:
10 loops, best of 3: 282 ms per loop
10 loops, best of 3: 35.9 ms per loop
我尝试选择一个例子来很好地展示这种效果。除非涉及到许多相对较小的数组上的NumPy函数调用,否则这不会改变时间。请记住,无论我们以哪种方式调用NumPy,Python函数调用仍然会发生。
这仅适用于NumPy中的函数。大多数数组方法对于memoryviews不可用(一些属性仍然可用,例如
size
、
shape
和
T
)。例如,使用NumPy数组的
A.dot(A.T)
将变为
np.dot(A, A.T)
。