Почему разница в производительности между numpy.zeros и numpy.zeros_like?

Я наконец нашел узкое место в производительности в своем коде, но не совсем понял, в чем причина. Чтобы решить это, я изменил все мои призывы numpy.zeros_like вместо того, чтобы использовать numpy.zeros, Но почему zeros_like оооочень намного медленнее?

Например (примечание e-05 на zeros вызов):

>>> timeit.timeit('np.zeros((12488, 7588, 3), np.uint8)', 'import numpy as np', number = 10)
5.2928924560546875e-05
>>> timeit.timeit('np.zeros_like(x)', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10)
1.4402990341186523

Но тогда странно писать в массив, созданный с zeros заметно медленнее, чем массив, созданный с zeros_like:

>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10)
0.4310588836669922
>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros_like(np.zeros((12488, 7588, 3), np.uint8))', number = 10)
0.33325695991516113

Мое предположение zeros использует какой-то трюк с процессором и фактически не записывает в память для его выделения. Это делается на лету, когда это написано. Но это все еще не объясняет огромное расхождение во времени создания массива.

Я использую Mac OS X Yosemite с текущей версией:

>>> numpy.__version__
'1.9.1'

2 ответа

Решение

Мой тайминги в Ipython (с более простым интерфейсом timeit):

In [57]: timeit np.zeros_like(x)
1 loops, best of 3: 420 ms per loop

In [58]: timeit np.zeros((12488, 7588, 3), np.uint8)
100000 loops, best of 3: 15.1 µs per loop

Когда я смотрю на код с IPython (np.zeros_like??) Я вижу:

res = empty_like(a, dtype=dtype, order=order, subok=subok)
multiarray.copyto(res, 0, casting='unsafe')

в то время как np.zeros это черный ящик - чистый скомпилированный код

Сроки для empty являются:

In [63]: timeit np.empty_like(x)
100000 loops, best of 3: 13.6 µs per loop

In [64]: timeit np.empty((12488, 7588, 3), np.uint8)
100000 loops, best of 3: 14.9 µs per loop

Так что дополнительное время в zeros_like в том copy,

В моих тестах разница во времени назначения (x[]=1) незначительный.

Я думаю, что zeros, ones, empty все ранние скомпилированные творения. empty_like было добавлено для удобства, просто рисуя информацию о форме и типе из ее ввода. zeros_like был написан с большим вниманием к простому сопровождению программирования (повторное использование empty_like), чем для скорости.

np.ones а также np.full также использовать np.empty ... copyto последовательность и показать аналогичные тайминги.


https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/array_assign_scalar.c представляется файлом, который копирует скаляр (например, 0) в массив. Я не вижу смысла memset,

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/alloc.c приглашает malloc а также calloc,

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/ctors.c - источник для zeros а также empty, Оба зовут PyArray_NewFromDescr_int, но один заканчивает тем, что использовал npy_alloc_cache_zero и другие npy_alloc_cache,

npy_alloc_cache в alloc.c звонки alloc, npy_alloc_cache_zero звонки npy_alloc_cache с последующим memset, Код в alloc.c далее путается с опцией THREAD.

Подробнее о calloc v malloc+memset Разница в: почему malloc+memset медленнее, чем calloc?

Но с кешированием и сборкой мусора, интересно calloc/memset различие применяется.


Этот простой тест с memory_profile Пакет поддерживает утверждение, что zeros а также empty распределять память "на лету", в то время как zeros_like выделяет все сразу:

N = (1000, 1000) 
M = (slice(None, 500, None), slice(500, None, None))

Line #    Mem usage    Increment   Line Contents
================================================
     2   17.699 MiB    0.000 MiB   @profile
     3                             def test1(N, M):
     4   17.699 MiB    0.000 MiB       print(N, M)
     5   17.699 MiB    0.000 MiB       x = np.zeros(N)   # no memory jump
     6   17.699 MiB    0.000 MiB       y = np.empty(N)
     7   25.230 MiB    7.531 MiB       z = np.zeros_like(x) # initial jump
     8   29.098 MiB    3.867 MiB       x[M] = 1     # jump on usage
     9   32.965 MiB    3.867 MiB       y[M] = 1
    10   32.965 MiB    0.000 MiB       z[M] = 1
    11   32.965 MiB    0.000 MiB       return x,y,z

Современные ОС выделяют память виртуально, т. Е. Память отдается процессу только при первом ее использовании. zeros получает память от операционной системы, чтобы ОС обнуляла ее при первом использовании. zeros_like с другой стороны, заполненная память заполняется нулями сама по себе. Оба способа требуют примерно одинакового объема работы - это просто zeros_like обнуление выполняется заранее, тогда как zeros в конечном итоге делает это на лету.

Технически, в C разница вызывает calloc против malloc+memset,

Другие вопросы по тегам