Расхождение в производительности между делением лога и вычитанием лога с помощью numba - PullRequest
0 голосов
/ 04 февраля 2020

Я пытаюсь оптимизировать некоторый код, который использует журналы (математический тип, а не вид записей временных меток :)), и я обнаружил что-то странное, что я не смог найти ответы в Интернете. У нас есть 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% медленнее, чем деление.

Кто-нибудь может объяснить эти расхождения?

  1. Почему вычитание журналов происходит намного быстрее, чем деление журналов для тривиального случая?

  2. Почему вычитание журналов на 40% медленнее, чем деление журналов, когда мы сохраняем значение в массиве? Я знаю, что при инициализации массива np.empty() существуют значительные затраты на установку - инициализация массива в subtract_log() в тривиальном случае, но без хранения значений в нем увеличивает время с 252 нс до 311 мкс.

1 Ответ

0 голосов
/ 04 февраля 2020

Не измеряйте «бесполезные» вещи, компилятор может полностью их оптимизировать

Если вы включите деление на ноль (error_model = "numpy"), оба функции занимают около 280 нс. Не из-за быстрых расчетов, а потому, что они на самом деле ничего не делают. Ожидается оптимизация от ненужных вычислений, но иногда LLVM не может обнаружить все это.

Во втором случае вы сравниваете время выполнения 2 логарифмов с 1 логарифмом и одним делением. (вычитания / сложения, а также умножения намного быстрее). Могут быть различия во времени расчета в зависимости от реализации журнала и процессора. Но также взгляните на результаты, они не совсем одинаковы.

По крайней мере, для подразделения floa64 (FDIV) вы можете взглянуть на таблицы инструкций от Agner Fog.

...