Расхождение в производительности между делением журнала и вычитанием журнала с использованием numba

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

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

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

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


person PyRsquared    schedule 04.02.2020    source источник


Ответы (1)


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

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

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

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

person max9111    schedule 04.02.2020