#python-3.x #performance #numpy #if-statement #optimization
Вопрос:
В моем коде я должен учитывать различные вклады в отношении разных пороговых значений. В частности, у меня есть функция my_index
, вывод которой должен быть сопоставлен с пороговыми Z_1, Z_2, Z_3
значениями, чтобы определить приращение к переменной my_value
. В следующем MWE, для простоты, функция my_index
является просто однородным генератором случайных чисел:
import numpy as np
my_len = 100000
Z_1 = 0.2
Z_2 = 0.4
Z_3 = 0.7
first = 1
second = 2
third = -0.0003
my_value = 0
for i in range(my_len):
my_index = np.random.uniform()
my_value = first*np.heaviside(my_index - Z_1,0)*np.heaviside(Z_2 - my_index,0) second*np.heaviside(my_index - Z_3,0) third*np.heaviside(Z_3 - my_index,0)
#if Z_1 < my_index < Z_2 add first
#if my_index > Z_3 add second
#if my_index < Z_3 add third
Я заменил if/else
те, которые могли бы использоваться для порогов с помощью функции Heaviside, см.. Имейте в виду, что в моем исходном коде этот раздел кода должен быть повторен до 10^5 раз.
Мой вопрос в следующем: делает ли эта практика код быстрее? Или вызов функции heaviside ( np.heaviside
) лучше с точки зрения скорости, чем управление if/else?
Комментарии:
1. Что вы обнаружили, когда протестировали его?
Ответ №1:
In [433]: x=np.arange(-10,10)
In [434]: x
Out[434]:
array([-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2,
3, 4, 5, 6, 7, 8, 9])
Правильное использование heaviside
— предоставление x
в виде массива, а не одного значения:
In [436]: np.heaviside(x,.5)
Out[436]:
array([0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.5, 1. , 1. ,
1. , 1. , 1. , 1. , 1. , 1. , 1. ])
Эквивалент понимания списка:
In [437]: [.5 if i==0 else (0 if i<0 else 1) for i in x]
Out[437]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1]
и создание массива из этого списка:
In [438]: np.array([.5 if i==0 else (0 if i<0 else 1) for i in x])
Out[438]:
array([0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.5, 1. , 1. ,
1. , 1. , 1. , 1. , 1. , 1. , 1. ])
Сравните времена:
In [439]: timeit np.heaviside(x,.5)
2.5 µs ± 17.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [440]: timeit np.array([.5 if i==0 else (0 if i<0 else 1) for i in x])
15.1 µs ± 25.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Итерация по списку выполняется быстрее (чем по массиву).:
In [441]: timeit np.array([.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()])
6.66 µs ± 195 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
и если мы пропустим преобразование обратно в список:
In [442]: timeit [.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()]
2.28 µs ± 3.01 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Для гораздо большего массива heaviside
производительность еще выше:
In [445]: x=np.arange(-1000,1000)
In [446]: timeit [.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()]
211 µs ± 7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [447]: timeit np.heaviside(x,.5)
13 µs ± 201 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Для генерации случайных чисел также быстрее использовать подход с использованием всего массива:
In [448]: timeit [np.random.uniform() for _ in range(1000)]
4.62 ms ± 20.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [449]: timeit np.random.uniform(1000)
4.74 µs ± 171 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Я также мог бы рассчитать время скалярного использования heaviside
— это хуже, чем if/else
в [446]:
In [450]: timeit [np.heaviside(i,.5) for i in x]
8.64 ms ± 44.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
В сумме:
- используйте код всего массива, где это возможно
- при использовании итерации на уровне Python вместо этого используйте списки и скалярные методы
Комментарии:
1. Я замечаю, что для больших массивов хевисайд работает лучше. А как насчет программирования без ветвей?
Ответ №2:
Предполагая, что вы используете стандартный интерпретатор CPython, то выполнение подобного вызова функции Numpy np.heaviside
, скорее всего, будет дороже, чем выполнение базовых условий. Однако и то, и другое очень неэффективно. Действительно, условные обозначения, как правило, работают медленно и могут быть заменены здесь реализацией без ветвей (добавление/умножение логических значений, преобразованных в целые числа). Наиболее важной оптимизацией является использование векторизации, поскольку Numpy спроектирован так, чтобы быть эффективным для относительно больших массивов, а не для скалярных значений (в основном из-за дополнительных внутренних проверок и вызовов функций). Вы можете сгенерировать все случайные значения в большом массиве, применить к нему функцию хевисайда несколько раз. Полученный код, безусловно, будет на 2 или 3 порядка быстрее!
Комментарии:
1. Это MWE, в котором я рассматривал индикатор просто как случайный параметр. Моя проблема не может быть векторизована. Однако реальной альтернативой является программирование без ветвей. Не могли бы вы, пожалуйста, предоставить реализацию программы без ветвей в этом MWE?