Причины различий в потреблении памяти и производительности np.zeros и np.full - PullRequest
6 голосов
/ 11 марта 2020

При измерении потребления памяти np.zeros:

import psutil
import numpy as np

process = psutil.Process()
N=10**8
start_rss = process.memory_info().rss
a = np.zeros(N, dtype=np.float64)
print("memory for a", process.memory_info().rss - start_rss)

результат неожиданный 8192 байт, т.е. почти 0, в то время как для дублирования 1e8 потребуется 8e8 байт.

При замене np.zeros(N, dtype=np.float64) на np.full(N, 0.0, dtype=np.float64) объем памяти, необходимый для a, составляет 800002048 байт.

Существуют похожие расхождения во времени выполнения:

import numpy as np
N=10**8
%timeit np.zeros(N, dtype=np.float64)
# 11.8 ms ± 389 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit np.full(N, 0.0, dtype=np.float64)
# 419 ms ± 7.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Т.е. np.zeros до В 40 раз быстрее для больших размеров.

Не уверен, что эти различия есть для всех архитектур / операционных систем, но я наблюдал это по крайней мере для x86-64 Windows и Linux.

Какие различия между np.zeros и np.full могут объяснить разное потребление памяти и разное время работы?

Ответы [ 2 ]

2 голосов
/ 11 марта 2020

Я не доверяю psutil для этих тестов памяти, и rss (Resident Set Size) может не соответствовать метрике c.

Использование stdlib tracemalloc Вы можете получить правильно выглядящие числа для выделения памяти - это должно быть приблизительно 800000000 байтов для этого N и типа float64:

>>> import numpy as np
>>> import tracemalloc
>>> N = 10**8
>>> tracemalloc.start()
>>> tracemalloc.get_traced_memory()  # current, peak
(159008, 1874350)
>>> a = np.zeros(N, dtype=np.float64)
>>> tracemalloc.get_traced_memory()
(800336637, 802014880)

Для временных разниц между np.full и np.zeros сравните справочные страницы для malloc и calloc, то есть np.zeros может go до подпрограммы выделения, которая получает обнуленные страницы . См. PyArray_Zeros -> вызовы PyArray_NewFromDescr_int, передающие 1 для аргумента zeroed, который затем имеет особый случай для распределения нулей быстрее:

if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) {
    data = npy_alloc_cache_zero(nbytes);
}
else {
    data = npy_alloc_cache(nbytes);
}

Похоже, np.full не имеет этого быстрого пути. Там производительность будет аналогична первой инициализации, а затем копированию O (n) :

a = np.empty(N, dtype=np.float64)
a[:] = np.float64(0.0)

numpy Разработчики, возможно, добавили бы быстрый путь к np.full если значение заполнения было нулевым, но зачем добавлять другой способ сделать то же самое - пользователи могут просто использовать np.zeros, во-первых.

0 голосов
/ 11 марта 2020

Функция numpy .zeros прямо использует кодовый слой C библиотеки Numpy, в то время как функции ones и full работают как то же самое, инициализируя массив значений и копируя в него нужное значение.

Тогда функция нули не нуждается в какой-либо языковой интерпретации, в то время как для других, единиц и полный , код Python необходимо интерпретировать как код C.

Взгляните на исходный код, чтобы понять его самостоятельно: https://github.com/numpy/numpy/blob/master/numpy/core/numeric.py

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...