Почему разница в производительности между 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
,