了解 Numba 性能差异

mgi*_*ert 6 numpy numba

我试图了解通过使用numba算法的各种实现所看到的性能差异。特别是,我希望func1d从下面开始是最快的实现,因为它是唯一不复制数据的算法,但是从我的时间来看func1b似乎是最快的。

\n
import numpy\nimport numba\n\n\ndef func1a(data, a, b, c):\n    # pure numpy\n    return a * (1 + numpy.tanh((data / b) - c))\n\n\n@numba.njit(fastmath=True)\ndef func1b(data, a, b, c):\n    new_data = a * (1 + numpy.tanh((data / b) - c))\n    return new_data\n\n\n@numba.njit(fastmath=True)\ndef func1c(data, a, b, c):\n    new_data = numpy.empty(data.shape)\n    for i in range(new_data.shape[0]):\n        for j in range(new_data.shape[1]):\n            new_data[i, j] = a * (1 + numpy.tanh((data[i, j] / b) - c)) \n    return new_data\n\n\n@numba.njit(fastmath=True)\ndef func1d(data, a, b, c):\n    for i in range(data.shape[0]):\n        for j in range(data.shape[1]):\n            data[i, j] = a * (1 + numpy.tanh((data[i, j] / b) - c)) \n    return data\n
Run Code Online (Sandbox Code Playgroud)\n

用于测试内存复制的辅助函数

\n
def get_data_base(arr):\n    """For a given NumPy array, find the base array\n    that owns the actual data.\n    \n    https://ipython-books.github.io/45-understanding-the-internals-of-numpy-to-avoid-unnecessary-array-copying/\n    """\n    base = arr\n    while isinstance(base.base, numpy.ndarray):\n        base = base.base\n    return base\n\n\ndef arrays_share_data(x, y):\n    return get_data_base(x) is get_data_base(y)\n\n\ndef test_share(func):\n    data = data = numpy.random.randn(100, 3)\n    print(arrays_share_data(data, func(data, 0.5, 2.5, 2.5)))\n
Run Code Online (Sandbox Code Playgroud)\n

时间安排

\n
# force compiling\ndata = numpy.random.randn(10_000, 300)\n_ = func1a(data, 0.5, 2.5, 2.5)\n_ = func1b(data, 0.5, 2.5, 2.5)\n_ = func1c(data, 0.5, 2.5, 2.5)\n_ = func1d(data, 0.5, 2.5, 2.5)\n\ndata = numpy.random.randn(10_000, 300)\n%timeit func1a(data, 0.5, 2.5, 2.5)\n%timeit func1b(data, 0.5, 2.5, 2.5)\n%timeit func1c(data, 0.5, 2.5, 2.5)\n%timeit func1d(data, 0.5, 2.5, 2.5)\n
Run Code Online (Sandbox Code Playgroud)\n
67.2 ms \xc2\xb1 230 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n13 ms \xc2\xb1 10.9 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 100 loops each)\n69.8 ms \xc2\xb1 60.4 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n69.8 ms \xc2\xb1 105 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

测试哪些实现复制内存

\n
# force compiling\ndata = numpy.random.randn(10_000, 300)\n_ = func1a(data, 0.5, 2.5, 2.5)\n_ = func1b(data, 0.5, 2.5, 2.5)\n_ = func1c(data, 0.5, 2.5, 2.5)\n_ = func1d(data, 0.5, 2.5, 2.5)\n\ndata = numpy.random.randn(10_000, 300)\n%timeit func1a(data, 0.5, 2.5, 2.5)\n%timeit func1b(data, 0.5, 2.5, 2.5)\n%timeit func1c(data, 0.5, 2.5, 2.5)\n%timeit func1d(data, 0.5, 2.5, 2.5)\n
Run Code Online (Sandbox Code Playgroud)\n
False\nFalse\nFalse\nTrue\n
Run Code Online (Sandbox Code Playgroud)\n

ead*_*ead 3

在这里,数据复制并没有发挥很大的作用:瓶颈在于函数的tanh计算速度。算法有很多种:有些更快,有些更慢,有些更精确,有些不太精确。

\n

不同的 numpy 发行版使用不同的tanh-function 实现,例如,它可以是 mkl/vml 中的一种,也可以是 gnu-math-library 中的一种。

\n

根据 numba 版本,还使用 ​​mkl/svml 实现或 gnu-math-library。

\n

查看内部的最简单方法是使用探查器,例如perf

\n

对于我机器上的 numpy 版本,我得到:

\n
>>> perf record python run.py\n>>> perf report\nOverhead  Command  Shared Object                                      Symbol                                  \n  46,73%  python   libm-2.23.so                                       [.] __expm1\n  24,24%  python   libm-2.23.so                                       [.] __tanh\n   4,89%  python   _multiarray_umath.cpython-37m-x86_64-linux-gnu.so  [.] sse2_binary_scalar2_divide_DOUBLE\n   3,59%  python   [unknown]                                          [k] 0xffffffff8140290c\n
Run Code Online (Sandbox Code Playgroud)\n

正如我们所看到的,numpy 使用慢速的 gnu-math-library ( libm) 功能。

\n

对于 numba 函数我得到:

\n
 53,98%  python   libsvml.so                                         [.] __svml_tanh4_e9\n   3,60%  python   [unknown]                                          [k] 0xffffffff81831c57\n   2,79%  python   python3.7                                          [.] _PyEval_EvalFrameDefault\n
Run Code Online (Sandbox Code Playgroud)\n

这意味着使用快速 mkl/svml 功能。

\n

这就是(几乎)全部内容。

\n
\n

正如 @user2640045 正确指出的那样,由于创建临时数组而导致额外的缓存未命中,numpy 性能将受到损害。

\n

然而,缓存未命中并没有像 的计算那样发挥如此大的作用tanh

\n
%timeit func1a(data, 0.5, 2.5, 2.5)  # 91.5 ms \xc2\xb1 2.88 ms per loop \n%timeit numpy.tanh(data)             # 76.1 ms \xc2\xb1 539 \xc2\xb5s per loop (mean \xc2\xb1 std. dev. of 7 runs, 10 loops each)\n
Run Code Online (Sandbox Code Playgroud)\n

即临时对象的创建占据了大约 20% 的运行时间。

\n
\n

FWIW,对于带有手写循环的版本,我的 numba 版本(0.50.1)能够矢量化并调用 mkl/svml 功能。如果对于其他版本,这种情况没有发生 - numba 将回退到 gnu-math-library 功能,这似乎是在您的计算机上发生的情况。

\n
\n

清单run.py

\n
>>> perf record python run.py\n>>> perf report\nOverhead  Command  Shared Object                                      Symbol                                  \n  46,73%  python   libm-2.23.so                                       [.] __expm1\n  24,24%  python   libm-2.23.so                                       [.] __tanh\n   4,89%  python   _multiarray_umath.cpython-37m-x86_64-linux-gnu.so  [.] sse2_binary_scalar2_divide_DOUBLE\n   3,59%  python   [unknown]                                          [k] 0xffffffff8140290c\n
Run Code Online (Sandbox Code Playgroud)\n