Я обратил внимание на то, что функция matmul
в numpy работает значительно хуже, чем функция dot
при умножении представлений массива. В этом случае мой вид массива является реальной частью сложного массива. Вот некоторый код, который воспроизводит проблему:
import numpy as np
from timeit import timeit
N = 1300
xx = np.random.randn(N, N) + 1j
yy = np.random.randn(N, N) + 1J
x = np.real(xx)
y = np.real(yy)
assert np.shares_memory(x, xx)
assert np.shares_memory(y, yy)
dot = timeit('np.dot(x,y)', number = 10, globals = globals())
matmul = timeit('np.matmul(x,y)', number = 10, globals = globals())
print('time for np.matmul: ', matmul)
print('time for np.dot: ', dot)
На моей машине вывод такой:
time for np.matmul: 23.023062199994456
time for np.dot: 0.2706864000065252
Это явно как-то связано с общей памятью, поскольку замена np.real(xx)
на np.real(xx).copy()
устраняет несоответствие производительности.
Троллинг документов numpy не особенно помог, поскольку перечисленные различия не обсуждали детали реализации при работе с представлениями памяти.
Связанный: github.com/numpy/numpy/issues/23123
@hpaulj Да, похоже на дубликат, за исключением того, что другой вопрос касается определенного типа данных. Я могу воспроизвести проблему с обычными поплавками и представлениями массива, такими как [::2,::2]
. Как упоминалось в проблеме github, связанной с @Chrysophylaxs, это похоже на ошибку с несмежными массивами.
Мои тайминги показывают, что dot
обычно использует copy
, когда входные данные не могут быть отправлены напрямую в BLAS. matmul
использует более медленный метод расчета.
Код типа BLAS может обрабатывать непрерывные данные только для ограниченного числа dtypes. numpy
разработчикам приходится выбирать между преобразованием других массивов в совместимую форму или использованием другого кода, что будет не так быстро.
Я отправил выпуск github по теме. Думаю, мы узнали здесь все, что могли.
Эти тайминги показывают, что dot
выполняет copy
с real
:
In [22]: timeit np.dot(xx.real,xx.real)
232 ms ± 3.34 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [23]: timeit np.dot(xx.real.copy(),xx.real.copy())
232 ms ± 4.18 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Применение этого к matmul
дает почти такое же время:
In [24]: timeit np.matmul(xx.real.copy(),xx.real.copy())
231 ms ± 3.54 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Опять же, matmul
с real
идут медленным путем. matmul/dot
оба работают хуже, когда им даются int
массивы, хотя и не так медленно, как в случае matmul real
. matmul/dot
также может обрабатывать object
dtypes, но это еще медленнее.
Таким образом, под обложкой происходит многое, чего мы, пользователи уровня Python, не видим (и не задокументировано).
У меня был соблазн изменить заголовок, чтобы сосредоточиться на комплексно-действительном, но я решил проверить другой view
- кусочек массива с плавающей запятой.
In [42]: y=xx.real.copy()[::2,::2];y.shape,y.dtype
Out[42]: ((650, 650), dtype('float64'))
In [43]: timeit np.dot(y,y)
36.4 ms ± 63.4 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [44]: timeit np.dot(y.copy(),y.copy())
35.6 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Опять же очевидно, что dot
использует copies
представления. matmul
не:
In [45]: timeit np.matmul(y,y)
1.89 s ± 3.01 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
но с копиями время такое же, как точка:
In [46]: timeit np.matmul(y.copy(),y.copy())
35.3 ms ± 102 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Итак, я предполагаю, что dot
обычно создает copy
, если он не может отправить массивы непосредственно в процедуры BLAS. matmul
вместо этого, очевидно, выбирает более медленный маршрут.
Хотя их обработка двумерных массивов аналогична, dot
и matmul
сильно различаются тем, как они обрабатывают трехмерные массивы. На самом деле основной причиной добавления @
было предоставление удобного «пакетного» понятия для умножения матриц.
Придерживаясь больших сложных массивов, давайте сделаем один в 3 раза больше:
In [49]: yy=np.array([xx,xx,xx]);yy.shape
Out[49]: (3, 1300, 1300)
In [50]: timeit np.dot(xx,xx)
794 ms ± 12.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [51]: timeit np.dot(xx,yy) # (yy,xx) same timings
55.5 s ± 151 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [52]: timeit np.matmul(xx,yy) # (yy,yy) same
2.58 s ± 362 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
matmul
только что увеличил время на 3; dot
к 70. Я мог бы больше исследовать вещи, но не с таймингами в минутном диапазоне.
Похоже, что matmul по умолчанию использует глупую реализацию O (n ^ 3) без учета кеша ЦП, если решает, что BLAS не может выполнять вычисления. Я думаю, это объясняет, почему существует такой большой разрыв в производительности с большими массивами. Действительно, похоже, что точка делает копию, чтобы вместо этого передать массив в BLAS. Все кажется последовательным. Остается только один вопрос... Почему это нигде не задокументировано и требует чтения исходного кода?
Возможный дубликат: stackoverflow.com/questions/64914877/…. Я пока не буду отмечать это как дубликат, если кто-то еще может добавить более точное объяснение.