Это связано с доступом к памяти и кэшированием. Каждая из этих функций делает две вещи, взяв в качестве примера первый код:
np.sum(arr > 0)
Сначала выполняется сравнение, чтобы найти, где arr
больше нуля (или не равно нулю, поскольку arr
содержит неотрицательные целые числа). Это создает промежуточный массив той же формы, что и arr
. Затем он суммирует этот массив.
Прямо, верно? Ну, при использовании np.sum(arr > 0)
это большой массив. Когда он достаточно большой, чтобы не помещаться в кэш, производительность снижается, поскольку, когда процессор начинает выполнять сумму, большинство элементов массива будет удалено из памяти и нуждается в перезагрузке.
Поскольку f_2
выполняет итерации по первому измерению, он имеет дело с меньшими подмассивами. То же самое копирование и сумма выполняются, но на этот раз промежуточный массив помещается в память. Он создан, использован и уничтожен, не покидая памяти. Это намного быстрее.
Теперь вы можете подумать, что f_3
будет самым быстрым (с использованием встроенного метода и всего), но, глядя на исходный код , вы увидите, что он использует следующие операции:
a_bool = a.astype(np.bool_, copy=False)
return a_bool.sum(axis=axis, dtype=np.intp
a_bool
- это просто еще один способ найти ненулевые записи, который создает большой промежуточный массив.
Выводы
Эмпирические правила - только это, и они часто неправильны. Если вам нужен более быстрый код, профилируйте его и посмотрите, в чем проблемы (хорошо поработайте над этим здесь).
Python
делает некоторые вещи очень хорошо. В случаях, когда он оптимизирован, он может быть быстрее, чем numpy
. Не бойтесь использовать старый старый код Python или типы данных в сочетании с Numpy.
Если вам часто приходится вручную писать циклы для повышения производительности, вы можете захотеть взглянуть на numexpr
- он автоматически делает это. Я сам этим не пользовался, но это должно обеспечить хорошее ускорение, если промежуточные массивы замедляют вашу программу.