Я пытаюсь оптимизировать код, который использует журналы (математический вид, а не тип записи с меткой времени :)), и я обнаружил что-то странное, что я не смог найти никаких ответов в Интернете. У нас есть log (a / b) = log (a) - log (b), поэтому я написал код для сравнения производительности двух методов.
import numpy as np
import numba as nb
# create some large random walk data
x = np.random.normal(0, 0.1, int(1e7))
x = abs(x.min()) + 100 + x # make all values >= 100
@nb.njit
def subtract_log(arr, tau):
"""arr is a numpy array, tau is an int"""
for t in range(tau, arr.shape[0]):
a = np.log(arr[t]) - np.log(arr[t - tau])
return None
@nb.njit
def divide_log(arr, tau):
"""arr is a numpy array, tau is an int"""
for t in range(tau, arr.shape[0]):
a = np.log(arr[t] / arr[t - tau])
return None
%timeit subtract_log(x, 100)
>>> 252 ns ± 0.319 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit divide_log(x, 100)
>>> 5.57 ms ± 48.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Итак, мы видим, что вычитание журналов происходит примерно в 20 000 раз быстрее, чем деление на журналы. Я нахожу это странным, потому что я мог подумать, что при вычитании журналов приближение логарифмического ряда должно быть вычислено дважды. Но, возможно, это как-то связано с тем, как операции numpy Broadcast?
Приведенный выше пример тривиален, поскольку мы ничего не делаем с результатом расчета. Ниже более реалистичный пример, в котором мы возвращаем результат расчета.
@nb.njit
def subtract_log(arr, tau):
"""arr is a numpy array, tau is an int"""
out = np.empty(arr.shape[0] - tau)
for t in range(tau, arr.shape[0]):
f = t - tau
out[f] = np.log(arr[t]) - np.log(arr[f])
return out
@nb.njit
def divide_log(arr, tau):
"""arr is a numpy array, tau is an int"""
out = np.empty(arr.shape[0] - tau)
for t in range(tau, arr.shape[0]):
f = t - tau
out[f] = np.log(arr[t] / arr[f])
return out
out1 = subtract_log(x, 100)
out2 = divide_log(x, 100)
np.testing.assert_allclose(out1, out2, atol=1e-8) # True
%timeit subtract_log(x, 100)
>>> 129 ms ± 783 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit divide_log(x, 100)
>>> 93.4 ms ± 257 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Теперь мы видим, что времена одинаковы по порядку величины, но вычитание бревен примерно на 40% медленнее, чем деление.
Кто-нибудь может объяснить эти расхождения?
Почему вычитание журналов намного быстрее, чем деление журналов в тривиальном случае?
Почему вычитание журналов на 40% медленнее, чем деление журналов, когда мы сохраняем значение в массиве? Я знаю, что инициализация массива
np.empty()
требует значительных затрат - инициализация массива вsubtract_log()
в тривиальном случае, но без хранения значений в нем увеличивает время с 252 нс до 311 мкс.