Cython:类型化的memoryview是否是类型化numpy数组的现代方法?

23

假设我想将一个numpy数组传递给一个cdef函数:

cdef double mysum(double[:] arr):
    cdef int n = len(arr)
    cdef double result = 0

    for i in range(n):
        result = result + arr[i]

    return result

这是处理NumPy数组的现代方式吗?请与此问题进行比较:cython / numpy type of an array

如果我想要做以下操作怎么办:

cdef double[:] mydifference(int a, int b):
    cdef double[:] arr_a = np.arange(a)
    cdef double[:] arr_b = np.arange(b)

    return arr_a - arr_b

这将返回一个错误,因为对于内存视图(memoryviews)而言,-未定义。那么,是否应该按照以下方式处理该情况?

cdef double[:] mydifference(int a, int b):
    arr_a = np.arange(a)
    arr_b = np.arange(b)

    return arr_a - arr_b

Cython支持numpy类型,详见文档。顺便提一下,我没有看到你链接问题的答案使用double[:],它是按照文档建议的方式实现的。 - simonzack
@simonzack 是的,我想展示一个例子,展示它如何使用文档中所述的方式。 - bzm3r
1个回答

40

我将引用文档(链接)中的内容:

Memoryviews与当前NumPy数组缓冲区支持(np.ndarray[np.float64_t,ndim=2])类似,但它们具有更多功能和更清晰的语法。

这表明Cython的开发人员认为内存视图是现代方法。

在优雅性和互操作性方面,内存视图相对于np.ndarray符号表示法具有一些重要优势,但它们在性能上并不占优势。

性能:

首先需要注意的是,boundscheck有时会无法与内存视图配合工作,从而导致带有boundscheck=True的内存视图出现人为快速的数字(即您可以获得快速且不安全的索引),如果您依赖于boundscheck来捕获错误,则可能会有一个令人讨厌的惊喜。

在大多数情况下,一旦应用了编译器优化,内存视图和numpy数组表示法在性能上是相等的,在差异存在时,通常也不超过10-30%。

性能基准测试

该数字是执行100,000,000个操作的时间(以秒为单位)。数字越小,速度越快。

ACCESS+ASSIGNMENT on small array (10000 elements, 10000 times)
Results for `uint8`
1) memory view: 0.0415 +/- 0.0017
2) np.ndarray : 0.0531 +/- 0.0012
3) pointer    : 0.0333 +/- 0.0017

Results for `uint16`
1) memory view: 0.0479 +/- 0.0032
2) np.ndarray : 0.0480 +/- 0.0034
3) pointer    : 0.0329 +/- 0.0008

Results for `uint32`
1) memory view: 0.0499 +/- 0.0021
2) np.ndarray : 0.0413 +/- 0.0005
3) pointer    : 0.0332 +/- 0.0010

Results for `uint64`
1) memory view: 0.0489 +/- 0.0019
2) np.ndarray : 0.0417 +/- 0.0010
3) pointer    : 0.0353 +/- 0.0017

Results for `float32`
1) memory view: 0.0398 +/- 0.0027
2) np.ndarray : 0.0418 +/- 0.0019
3) pointer    : 0.0330 +/- 0.0006

Results for `float64`
1) memory view: 0.0439 +/- 0.0037
2) np.ndarray : 0.0422 +/- 0.0013
3) pointer    : 0.0353 +/- 0.0013

ACCESS PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0576 +/- 0.0006
2) np.ndarray : 0.0570 +/- 0.0009
3) pointer    : 0.0061 +/- 0.0004

Results for `uint16`
1) memory view: 0.0806 +/- 0.0002
2) np.ndarray : 0.0882 +/- 0.0005
3) pointer    : 0.0121 +/- 0.0003

Results for `uint32`
1) memory view: 0.0572 +/- 0.0016
2) np.ndarray : 0.0571 +/- 0.0021
3) pointer    : 0.0248 +/- 0.0008

Results for `uint64`
1) memory view: 0.0618 +/- 0.0007
2) np.ndarray : 0.0621 +/- 0.0014
3) pointer    : 0.0481 +/- 0.0006

Results for `float32`
1) memory view: 0.0945 +/- 0.0013
2) np.ndarray : 0.0947 +/- 0.0018
3) pointer    : 0.0942 +/- 0.0020

Results for `float64`
1) memory view: 0.0981 +/- 0.0026
2) np.ndarray : 0.0982 +/- 0.0026
3) pointer    : 0.0968 +/- 0.0016

ASSIGNMENT PERFORMANCE (100,000,000 element array):
Results for `uint8`
1) memory view: 0.0341 +/- 0.0010
2) np.ndarray : 0.0476 +/- 0.0007
3) pointer    : 0.0402 +/- 0.0001

Results for `uint16`
1) memory view: 0.0368 +/- 0.0020
2) np.ndarray : 0.0368 +/- 0.0019
3) pointer    : 0.0279 +/- 0.0009

Results for `uint32`
1) memory view: 0.0429 +/- 0.0022
2) np.ndarray : 0.0427 +/- 0.0005
3) pointer    : 0.0418 +/- 0.0007

Results for `uint64`
1) memory view: 0.0833 +/- 0.0004
2) np.ndarray : 0.0835 +/- 0.0011
3) pointer    : 0.0832 +/- 0.0003

Results for `float32`
1) memory view: 0.0648 +/- 0.0061
2) np.ndarray : 0.0644 +/- 0.0044
3) pointer    : 0.0639 +/- 0.0005

Results for `float64`
1) memory view: 0.0854 +/- 0.0056
2) np.ndarray : 0.0849 +/- 0.0043
3) pointer    : 0.0847 +/- 0.0056

基准测试代码(仅显示访问和赋值)

# cython: boundscheck=False
# cython: wraparound=False
# cython: nonecheck=False
import numpy as np
cimport numpy as np
cimport cython

# Change these as desired.
data_type = np.uint64
ctypedef np.uint64_t data_type_t

cpdef test_memory_view(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_ndarray(np.ndarray[data_type_t, ndim=1] view):
    cdef Py_ssize_t i, j, n = view.shape[0]

    for j in range(0, n):
        for i in range(0, n):
            view[i] = view[j]

cpdef test_pointer(data_type_t [:] view):
    cdef Py_ssize_t i, j, n = view.shape[0]
    cdef data_type_t * data_ptr = &view[0]

    for j in range(0, n):
        for i in range(0, n):
            (data_ptr + i)[0] = (data_ptr + j)[0]

def run_test():
    import time
    from statistics import stdev, mean
    n = 10000
    repeats = 100
    a = np.arange(0, n,  dtype=data_type)
    funcs = [('1) memory view', test_memory_view),
        ('2) np.ndarray', test_ndarray),
        ('3) pointer', test_pointer)]

    results = {label: [] for label, func in funcs}
    for r in range(0, repeats):
        for label, func in funcs:
            start=time.time()
            func(a)
            results[label].append(time.time() - start)

    print('Results for `{}`'.format(data_type.__name__))
    for label, times in sorted(results.items()):
        print('{: <14}: {:.4f} +/- {:.4f}'.format(label, mean(times), stdev(times)))

这些基准测试表明,总体上性能差异不大。有时候 np.ndarray 符号略快,有时候则相反。

需要注意的一件事是,当代码变得稍微复杂或“真实”时,差异突然消失了,好像编译器失去了信心,无法应用一些非常聪明的优化。这可以从浮点数的性能中看出来,在其中根本没有任何区别,可能是因为一些花哨的整数优化无法使用。

易用性

内存视图具有显着优势,例如您可以在 numpy 数组、CPython 数组、cython 数组、c 数组和更多的数组上使用内存视图,无论是现在还是将来都可以。另外,还有一个简单的并行语法,可以将任何内容转换为内存视图:

cdef double [:, :] data_view = <double[:256, :256]>data

内存视图在这方面非常好,因为如果您将函数定义为接受内存视图,则它可以接受其中任何一种东西。这意味着您可以编写一个不依赖于numpy的模块,但仍然可以使用numpy数组。

另一方面,np.ndarray 表示的是仍然是一个numpy数组,您可以对其调用所有numpy数组方法。然而,同时拥有numpy数组和该数组的视图并不是个大问题:

def dostuff(arr):
    cdef double [:] arr_view = arr
    # Now you can use 'arr' if you want array functions,
    # and arr_view if you want fast indexing

同时使用数组和数组视图在实践中效果很好,我非常喜欢这种风格,因为它清晰地区分了Python级别的方法和C级别的方法。

结论

性能几乎相同,肯定没有足够的差异可以成为决定因素。

NumPy数组符号更接近加速Python代码而不会对其进行太多更改的理想情况,因为您可以继续使用相同的变量,同时获得全速数组索引。

另一方面,内存视图符号可能是未来趋势。如果您喜欢它的优雅,并且使用不同类型的数据容器而不仅仅是NumPy数组,则有充分的理由出于一致性的考虑使用内存视图。


2
嗨Bhante - 非常感谢!像往常一样,你的回答非常好。我现在真的很好奇,但你是从哪里学到Cython的呢?对我来说,它似乎文档不是很好,但也许这是我的经验不足在说话?例如,考虑“使用numpy”部分:有一个句子提到了使用np.ndarray(示例中有一个重复使用),但它甚至没有讨论与该类型规范相关的关键字。除了反复询问您的评论之外,我去哪里才能获得正确的理解呢? :p - bzm3r
1
@user89 我通过尝试和追踪源代码来学习Cython。例如,如果您想知道libcpp容器支持什么,您几乎必须查看Include/libcpp/*.pxd文件。Cython的许多内容没有文档记录,它只是按照自己的方式运行。cython -a有助于更好地理解Cython的工作原理。了解至少一点c语言也会有所帮助。 - Blake Walsh
1
@user89,我更新了这个答案,因为我之前说的关于内存视图速度较慢的观点在更严格的基准测试中被证明是完全错误的,我已经将测试结果添加到了答案中。 - Blake Walsh
Memoryviews 对于我的测试案例快了好几倍。如果你在最后一个索引上使用 ::1 关闭步幅计算,那么它会消除大量的步幅计算。 - xioxox
1
@Rok 不知道他们的文档怎么了,但你可以在 GitHub 上找到它 https://github.com/cython/cython/blob/master/docs/src/userguide/memoryviews.rst - Blake Walsh
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接