我对此的了解仍然不完善,但这可能会有所帮助。
我运行了一些非正式的基准测试来展示每种数组类型的优点,并对我的发现很感兴趣。
虽然这些数组类型在很多方面都不同,但如果您正在使用大型数组进行大量计算,您应该能够从其中任何一个中获得相似的性能,因为逐项访问应该大致相同板。
NumPy 数组是使用 Python 的 C API 实现的 Python 对象。
NumPy 数组确实提供了 C 级别的 API,但它们不能独立于 Python 解释器创建。
它们特别有用,因为 NumPy 和 SciPy 中提供了所有不同的数组操作例程。
Cython 内存视图也是一个 Python 对象,但它是作为 Cython 扩展类型制作的。
它似乎不是为在纯 Python 中使用而设计的,因为它不是 Cython 的一部分,可以直接从 Python 导入,但您可以从 Cython 函数将视图返回给 Python。
你可以在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
# An inlined dot product
@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
# non-inlined dot-product
@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
# function calling inlined dot product
@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
# function calling non-inlined version
@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
# inlined dot product using numpy arrays
@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
# non-inlined dot product using numpy arrays
@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
# function calling inlined numpy array dot product
@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
# function calling nun-inlined version for numpy arrays
@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
# Version with explicit looping and item-by-item access.
@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)能够处理这些优化,而不会被告知内联某些函数。
即使没有显式内联,Memoryview 也可以更快地在 Cython 模块内的函数之间传递数组切片。
然而,在这种特殊情况下,即使将循环推到 C 中,也无法真正达到通过正确使用矩阵乘法所能达到的速度。
BLAS 仍然是执行此类操作的最佳方式。
%timeit A.dot(A.T)
10 loops, best of 3: 25.7 ms per loop
还有从 NumPy 数组到内存视图的自动转换,如
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 数组,分配时间可能要快一些。
其他数组类型的设计速度几乎一样快且更方便,因此我建议您使用它们,除非有令人信服的理由不这样做。
更新:正如@Veedrac 的回答中提到的,您仍然可以将 Cython 内存视图传递给大多数 NumPy 函数。
当你这样做时,NumPy 通常必须创建一个新的 NumPy 数组对象来处理内存视图,所以这会有点慢。
对于大型阵列,影响可以忽略不计。
无论数组大小如何,调用np.asarray 以获得内存视图都会相对较快。
但是,为了演示这种效果,这里有另一个基准:
Cython 文件:
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 中的函数。
大多数数组方法不适用于内存视图(一些属性仍然可用,如size 和shape 和T)。
例如,带有 NumPy 数组的 A.dot(A.T) 将变为 np.dot(A, A.T)。