Я пытаюсь оптимизировать некоторый код, который использует журналы (математический тип, а не вид записей временных меток :)), и я обнаружил что-то странное, что я не смог найти ответы в Интернете. У нас есть 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)
Итак, мы видим, что вычитание журналов - это ~ В 20000 раз быстрее, чем деление на логи. Я нахожу это странным, потому что я подумал бы, что при вычитании бревен приближение ряда бревен должно быть вычислено дважды . Но, возможно, это как-то связано с тем, как numpy транслирует операции?
Приведенный выше пример тривиален, поскольку мы ничего не делаем с результатом вычисления. Ниже приведен более реалистичный пример c, в котором мы возвращаем результат вычисления.
@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 мкс.