首页 > 解决方案 > 使用 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 广播操作的方式有关?

上面的例子很简单,因为我们没有对计算结果做任何事情。下面是一个更现实的例子,我们返回计算结果。

@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()在简单的情况下初始化一个数组,但是如果不将值存储在其中会使时间从 252ns 增加到 311us。

标签: pythonperformancenumpynumba

解决方案


不要测量“无用”的东西,编译器可能会完全优化它

如果您关闭除零检查 (error_model="numpy"),这两个函数都需要大约 280ns。不是因为计算速度快,而是因为他们实际上什么也没做。期望优化掉无用的计算,但有时 LLVM 无法检测到所有这些。

在第二种情况下,您将 2 个对数的运行时间与 1 个对数和一个除法进行比较。(减法/加法以及乘法要快得多)。计算时间可能会有所不同,具体取决于日志实现和处理器。但也看看结果,它们并不完全相同。

至少对于 floa64 部门 (FDIV),您可以查看 Agner Fog 的指令表


推荐阅读