【问题标题】:C array vs NumPy arrayC 数组与 NumPy 数组
【发布时间】:2014-03-22 04:01:56
【问题描述】:

在性能(代数运算、查找、缓存等)方面,C 数组(可以暴露为 C 数组,或 cython.view.array[Cython array],或者上述两个的 memoryview) 和一个 NumPy 数组(在 Cython 中应该没有 Python 开销)

编辑:

我应该提到,在 NumPy 数组中是使用 Cython 静态类型的,dtypes 是 NumPy 编译时数据类型(例如 cdef np.int_tcdef np.float32_t),而 C 案例中的类型是 C等价物(cdef int_tcdef float

编辑2:

这是来自Cython Memoryview documentation 的示例,以进一步说明我的问题:

from cython.view cimport array as cvarray
import numpy as np

# Memoryview on a NumPy array
narr = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
cdef int [:, :, :] narr_view = narr

# Memoryview on a C array
cdef int carr[3][3][3]
cdef int [:, :, :] carr_view = carr

# Memoryview on a Cython array
cyarr = cvarray(shape=(3, 3, 3), itemsize=sizeof(int), format="i")
cdef int [:, :, :] cyarr_view = cyarr

坚持使用C arrayCython arrayNumPy array 之间有什么区别吗?

【问题讨论】:

  • 你去基准测试然后回来告诉我们怎么样?

标签: python c arrays numpy cython


【解决方案1】:

我对此的了解仍然不完善,但这可能会有所帮助。 我运行了一些非正式的基准测试来展示每种数组类型的优点,并对我的发现很感兴趣。

虽然这些数组类型在很多方面都不同,但如果您正在使用大型数组进行大量计算,您应该能够从其中任何一个中获得相似的性能,因为逐项访问应该大致相同板。

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 中的函数。 大多数数组方法不适用于内存视图(一些属性仍然可用,如sizeshapeT)。 例如,带有 NumPy 数组的 A.dot(A.T) 将变为 np.dot(A, A.T)

【讨论】:

  • "NumPy 数组是一个带有 C API 的 Python 对象。"你的意思是反过来吗?
  • @asmeurer 不。我不会反过来说,因为我不想暗示 NumPy 数组可以在没有 Python 的情况下使用。尽管它们是使用 Python 的 C API 在 C 中实现的,但它们实际上仍然是 Python 对象,不能独立于 Python 解释器使用。我在紧接您指出的那句话之后澄清了这一点。不过,谢谢你提出这个问题,我会试着想一个更好的方式来表达第一句话。
  • ptr_time() 版本可能比您使用的 carr_time() 慢,range(size) 而不是 range(10000),在 x86 上,编译器在使用内存之前有 4 个寄存器,即 eax = i , ebx = AP, ecx = j, edx = n, size 没有剩余,因此会发生额外的内存操作,size 是 dword[ebp-8],ebp 是堆栈指针。在 carr 版本中,除了循环与常量进行比较之外,代码是相同的。如果你改变它,它应该在理论上匹配。但是根据 cython 生成的内容和 c 编译器,可能仍然存在差异。
【解决方案2】:

不要使用cython.view.array,使用cpython.array.array

有关详细信息,请参阅this answer of mine,尽管这仅涉及速度。建议将cython.view.array 视为“演示”材料,将cpython.array.array 视为实际可靠的实现。这些数组非常轻量级,仅将它们用作暂存空间时效果更好。

此外,如果您曾经受过 malloc 的诱惑,对它们的原始访问不会变慢,并且实例化只需要两倍的时间。


关于 IanH 的

如果您需要给定数组的数组切片和 NumPy 功能,您可以创建一个内存视图,指向与 NumPy 数组相同的内存。

值得注意的是,memoryviews 有一个“base”属性,许多 Numpy 函数也可以采用 memoryviews,因此这些不必是单独的变量。

【讨论】:

  • 好点。感谢您指出这些。我已经用一些关于使用 NumPy 函数的 cmets 更新了我的答案。
猜你喜欢
  • 2022-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-04
  • 2018-05-29
相关资源
最近更新 更多