Почему вычитание быстрее при выполнении арифметики с массивом Numpy и int по сравнению с использованием векторизации с двумя массивами Numpy? - PullRequest
1 голос
/ 19 октября 2019

Я не понимаю, почему этот код:

start = time.time()
for i in range(1000000):
    _ = 1 - np.log(X)
print(time.time()-start)

Выполняется быстрее, чем эта реализация:

start = time.time()
for i in range(1000000):
    _ = np.subtract(np.ones_like(X), np.log(X))
print(time.time()-start)

Насколько я понимаю, это должно быть наоборот, как во второмРеализация Я использую ускорение, обеспечиваемое векторизацией, поскольку она способна управлять элементами в X одновременно, а не последовательно, как я и предполагал в первых функциях реализации.

Может кто-то пролить свет наэто для меня, как я искренне запутался? Спасибо!

Ответы [ 4 ]

5 голосов
/ 19 октября 2019

Обе версии вашего кода одинаково векторизованы. Массив, который вы создали, чтобы попытаться векторизовать вторую версию, просто лишний.


Векторизация NumPy не относится к векторизации оборудования. Если компилятор достаточно умен, он может в конечном итоге использовать аппаратную векторизацию, но NumPy явно не использует AVX или что-либо еще.

Векторизация NumPy относится к написанию кода уровня Python, который работает сразу с целыми массивами, а неиспользуя аппаратные инструкции, которые работают с несколькими операндами одновременно. Это векторизация на уровне Python, а не на уровне машинного языка. Преимущество этого по сравнению с написанием явных циклов состоит в том, что NumPy может выполнять работу в циклах уровня C вместо Python, избегая огромного количества динамической отправки, упаковки, распаковки, отключений через цикл оценки байт-кода и т. Д.

Обе версии вашего кода в этом смысле векторизованы, но вторая тратит кучу памяти и пропускную способность памяти при записи и чтении гигантского массива из них.

Кроме того, даже если мы говорили об аппаратномВекторизация уровня, версия 1 - будет так же поддаётся векторизации аппаратного уровня, как и другая версия. Вы просто загрузите скаляр 1 во все позиции векторного регистра и продолжите как обычно. Это потребует гораздо меньшего количества передач в память и из памяти, чем вторая версия, поэтому, вероятно, будет работать быстрее, чем вторая версия.

1 голос
/ 19 октября 2019

Время практически одинаковое. Как отмечают другие, здесь нет какого-либо аппаратного обеспечения или многоядерного распараллеливания, а просто смесь интерпретируемых функций Python и numpy.

In [289]: x = np.ones((1000,1000))

In [290]: timeit 1-np.log(x)                                                    
15 ms ± 1.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [291]: timeit np.subtract(np.ones_like(x), np.log(x))                        
18.6 ms ± 1.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Извлеките np.ones_like из цикла синхронизации:

In [292]: %%timeit y = np.ones_like(x) 
     ...: np.subtract(y,np.log(x)) 
     ...:  
     ...:                                                                       
15.7 ms ± 441 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

2/3 времени тратится в функции log:

In [303]: timeit np.log(x)                                                      
10.7 ms ± 211 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [306]: %%timeit y=np.log(x) 
     ...: np.subtract(1, y)                                                                  
3.77 ms ± 5.16 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Изменения в том, как генерируется 1, составляют незначительную часть времени.

С помощью «широковещания» математику с помощью скаляра и массива или массива и массива можно легко выполнить.

1, скалярное (фактически массив с формой ()), транслируется на (1,1), а затем на (1000,1000), все это без копирования.

0 голосов
/ 19 октября 2019

Случай A запускает только один итератор в mpu, тогда как случай B имеет два итератора по двум векторам размером X, что требует загрузки переключения контекста в потоке, если не оптимизирован. Случай B является более общей версией случая A ...

0 голосов
/ 19 октября 2019

Я, конечно, не эксперт по пустякам, но я думаю, что в первом примере используется только один вектор, а во втором фактически сначала создается вектор 1, а затем вычитается. Для последнего требуется двойной объем памяти и один дополнительный шаг для создания вектора 1.

На процессоре x86 обе, вероятно, являются своего рода инструкциями AVX, которые работают с 4 числами одновременно. Если, конечно, вы не используете необычный процессор с SIMD-шириной, превышающей длину вашего вектора, и этот процессор поддерживается numpy.

...