2017-10-01 75 views
7

Potrzebuję zoptymalizować skrypt, który intensywnie wykorzystuje obliczanie wektorów L1. Jak wiemy, norma L1 w tym przypadku jest po prostu sumą wartości bezwzględnych. Kiedy czas, jak szybko numpy jest w tym zadaniu, znalazłem coś dziwnego: dodanie wszystkich elementów wektorowych jest około 3 razy szybsze niż przyjęcie bezwzględnej wartości każdego elementu wektora. Jest to zaskakujący wynik, ponieważ dodanie jest dość skomplikowane w porównaniu do przyjmowania wartości bezwzględnej, co wymaga jedynie zerowania co 32-tego bloku danych (zakładając float32).Dlaczego plik numpy.absolute() jest tak wolny?

Dlaczego ten dodatek jest 3x szybszy niż prosta operacja bitowa?

import numpy as np 

a = np.random.rand(10000000) 

%timeit np.sum(a) 
13.9 ms ± 87.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 

%timeit np.abs(a) 
41.2 ms ± 92.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 
+0

Bardzo prawdopodobne z powodu prognozy branży - https://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array – Maxim

+0

Doesn ' t pokazuje jakiekolwiek przyspieszenie podczas sortowania tablicy wejściowej. – Lugi

Odpowiedz

2

np.sum zwraca wartość skalarną. np.abs zwraca nową tablicę o tym samym rozmiarze. Przydzielenie pamięci dla tej nowej tablicy zajmuje najwięcej czasu. Porównaj

>>> timeit("np.abs(a)", "import numpy as np; a = np.random.rand(10000000)", number=100) 
3.565487278989167 
>>> timeit("np.abs(a, out=a)", "import numpy as np; a = np.random.rand(10000000)", number=100) 
0.9392949139873963 

Argument out=a mówi NumPy umieścić wynik w tej samej tablicy a, zastępując stare dane tam. Stąd przyspieszenie.

Sum jest wciąż nieco szybciej:

>>> timeit("np.sum(a)", "import numpy as np; a = np.random.rand(10000000)", number=100) 
0.6874654769926565 

ale nie wymaga tak dużo zapisu dostępu do pamięci.

Jeśli nie chcesz nadpisać wartości a, możesz podać inną tablicę dla wyjścia abs, zakładając, że musisz wielokrotnie pobierać abs z tablic tego samego typu i rozmiaru.

b = np.empty_like(a) # done once, outside the loop 
np.abs(a, out=b) 
np.sum(b) 

przebiega w około połowie czasu np.linalg(a, 1)

odsyłającym np.linalg oblicza normę L1 jako

add.reduce(abs(x), axis=axis, keepdims=keepdims) 

która obejmuje alokację pamięci na nową tablicę abs(x).


Optymalnym rozwiązaniem byłby sposób obliczyć sumę (lub maksymalnych lub minimalnych) wszystkich wartościach bezwzględnych (lub wynikiem innego „ufunc”), bez przenoszenia wszystkie komunikaty w pamięci RAM, a następnie przez pobranie do sumy/maks./min. Było trochę dyskusji w repozytorium NumPy, ostatnio w add a max_abs ufunc, ale nie zostało ono wdrożone.

Sposób ufunc.reduce dostępne dla funkcji z dwoma wejściami jak add lub logaddexp, ale nie ma funkcji addabs (x, y : x+abs(y)), aby zmniejszyć o.

+0

Wierzę, że funkcja, którą podałeś, jest 2x razy szybsza niż norma np.linalg l1, wciąż wykonuje wiele niepotrzebnych operacji. np.abs (a, out = b) przenosi wynik do pamięci RAM, a następnie np.sum (b) odczytuje wynik z pamięci RAM, czy moje myślenie jest tutaj poprawne? – Lugi

+0

Prawidłowe. Najlepiej byłoby, gdybyśmy mieli 'addabs' dla' adduns 'dla "dodaj wartość absolutną" i uruchom [ufunc.reduce (a)] (https://docs.scipy.org/doc/numpy-1.13.0/reference/ generated/numpy.ufunc.reduce.html), lub po prostu użyj metody tablicy 'abssum', która nie tworzy tablicy pośredniej. To było krótko [omówione w NumPy tracker problem] (https://github.com/numpy/numpy/issues/5218) kilka lat temu. – FTP

3

Jest kilka rzeczy do rozważenia. sum zwraca wartość skalarną abs zwraca tablicę. Więc nawet jeśli dodanie dwóch liczb i przyjęcie absolutu miałoby tę samą szybkość, to abs byłoby wolniejsze, ponieważ musi utworzyć tablicę. I musi przetwarzać dwa razy więcej elementów (odczyt z wejścia + zapis do wyjścia).

Tak więc z tych czasów nie można wnioskować niczego o szybkości dodawania w porównaniu do operacji bitowej.

Można jednak sprawdzić, czy to szybciej coś dodać do każdej wartości tablicy w porównaniu z podjęciem bezwzględna każdej wartości

%timeit a + 0.1 
9 ms ± 155 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 
%timeit abs(a) 
9.98 ms ± 532 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 

Albo porównać sumy + alokacji pamięci w porównaniu z podjęciem absolutną

%timeit np.full_like(a, 1); np.sum(a) 
13.4 ms ± 358 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 
%timeit abs(a) 
9.64 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 

Tylko w przypadku, gdy chcesz, aby obliczanie normą szybciej, można spróbować Numba (lub Cython lub pisząc C lub Fortran rutynowych siebie), w ten sposób można uniknąć jakichkolwiek przydziałów pamięci:

import numba as nb 

@nb.njit 
def sum_of_abs(arr): 
    sum_ = 0 
    for item in arr: 
     sum_ += abs(item) 
    return sum_ 

sum_of_abs(a) # one call for the jitter to kick in 
%timeit sum_of_abs(a) 
# 2.44 ms ± 315 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 
+0

(Faktycznie zrobiłem tablicę wejściową 10 razy większą, więc czasy tutaj są 10 razy dłuższe w porównaniu do OP). Przy alokacji sumy i pamięci: 142 ms ± 1.97 ms Dla wartości bezwzględnej: 428 ms ± 20,4 ms Zwykły przydział pamięci:% timeit np.empty_like (a) 6.44 μs ± 22,8 ns, więc przydzielanie pamięci wydaje się zajmować mało czasu. – Lugi

+0

@Lugi I zaktualizował odpowiedź 'np.empty_like' faktycznie może" podrobić "przydzielanie tablicy (w zależności od systemu operacyjnego). Użyłem teraz 'np.full_like' ponieważ to również" emuluje "podwójną przepustowość pamięci. Jednak porównania nadal nie są "uczciwe" w tym sensie, że faktycznie pozwalają na wnioski dotyczące prędkości "abs" w stosunku do "addycji". – MSeifert