Подтвердить что ты не робот

Cython: типизированы memoryviews современный способ ввода numpy массивов?

Скажем, я хотел бы передать массив 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 массива

Что делать, если я хочу сделать следующее:

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

Это вернет ошибку, потому что - не определен для памяти. Итак, должен ли этот случай быть обработан следующим образом?

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

    return arr_a - arr_b
4b9b3361

Ответ 1

Я приведу из docs документы

Память похожа на текущую поддержку буфера массива NumPy (np.ndarray[np.float64_t, ndim=2]), но у них больше функций и более чистый синтаксис.

Это указывает на то, что разработчики Cython считают, что представления памяти являются современными.

Представления памяти имеют некоторые большие преимущества перед нотами np.ndarray в первую очередь в элегантности и функциональной совместимости, однако они не превосходят производительность.

Производительность:

Во-первых, следует отметить, что boundscheck иногда не работает с представлениями памяти, что приводит к искусственно быстрым цифрам для memviews с 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, но который все еще может принимать несколько массивов.

С другой стороны, нотация 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, существует очень веская причина для использования соображений памяти для обеспечения согласованности.