【问题标题】:When numba is effective?什么时候numba有效?
【发布时间】:2019-03-29 12:18:55
【问题描述】:

我知道 numba 会产生一些开销,并且在某些情况下(非密集计算)它会比纯 python 慢。但我不知道在哪里画线。是否可以使用算法复杂度的顺序来确定在哪里?

例如,在此代码中添加两个比 5 短的数组 (~O(n)),纯 python 更快:

def sum_1(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

@numba.jit('float64[:](float64[:],float64[:])')
def sum_2(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

# try 100
a = np.linspace(1.0,2.0,5)
b = np.linspace(1.0,2.0,5)
print("pure python: ")
%timeit -o sum_1(a,b)
print("\n\n\n\npython + numba: ")
%timeit -o sum_2(a,b)

更新:我正在寻找类似here 的指南:

“一般准则是为不同的数据大小和算法选择不同的目标。“cpu”目标适用于小数据大小(大约小于 1KB)和低计算强度算法。它的开销最少. “并行”目标适用于中等数据大小(大约小于 1MB)。线程增加了小延迟。“cuda”目标适用于大数据大小(大约大于 1MB)和高计算强度算法。将内存传入和传出 GPU 会增加大量开销。”

【问题讨论】:

  • 在测量 numba 的性能时,您需要在测量前调用该函数一次。因为在第一次调用期间,函数被编译,这需要很多时间。你也可以试试@numba.autojit
  • 在我的机器上 python: 3.31 µs, numba: 589 ns, 虽然不是很大的改进。至于您的问题,我真的认为这与复杂性并没有真正的关系,它可能取决于您正在执行的操作类型。另一方面,您仍然可以绘制 python/numba 比较,以查看给定函数发生偏移的位置。我也对一个玩具示例做了一些测试,向数组中的所有元素添加一个,numba 总是比 python 快,并且类似于 cython。
  • @cglacet 感谢您的回复。是的,我在基准测试之前运行它,(毕竟它只是及时编译:))。关于您的第二条评论@numba.autojit 不会重现您所得到的。对于阵列 len 5,它们的运行时间几乎相同。也许我没有把它放在正确的位置。我试过这个@numba.jit('float64[:](float64[:],float64[:])')
  • 您真的要显式声明非连续数组,同时提供连续数组进行测试吗?为输出声明一个数组也很奇怪,因为结果是一个标量。对于这类问题,您通常会让 Numba 确定数组声明,然后添加一个 fastmath 标志 @numba.njit(fastmath=True)(使 SIMD-Vectorization 成为可能),也许还有一个 cache=True 用于缓存已编译的函数。已更正,该函数在任何情况下都应该比@MSeifert 的numpy_methods 方法运行得更快。

标签: python python-3.x performance numba


【解决方案1】:

运行此代码可使我的机器加速约 6 倍:

@numba.autojit
def sum_2(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

Python:3.31 µs,numba:589 ns。

至于您的问题,我真的认为这与复杂性无关,它可能主要取决于您正在执行的操作类型。另一方面,您仍然可以绘制 python/numba 比较,以查看给定函数发生偏移的位置。

【讨论】:

  • 亲爱的@cglacet,感谢您的回答,我认为我正在寻找的是像下一条评论这样的指南,但对于操作强度或类似的东西
  • “一般准则是为不同的数据大小和算法选择不同的目标。“cpu”目标适用于小数据大小(大约小于 1KB)和低计算强度算法。它有开销最少。“并行”目标适用于中等数据大小(大约小于 1MB)。线程增加了一个小延迟。“cuda”目标适用于大数据大小(大约大于 1MB)和高计算强度算法。将内存传入和传出 GPU 会增加大量开销。” numba.pydata.org/numba-doc/latest/user/vectorize.html
  • 顺便说一句,您的回答很有帮助,但我让我们看看还能学到什么。长寿和繁荣;-)
【解决方案2】:

如果您不完全知道显式输入和输出声明的结果是什么,请让 numba 来决定。根据您的输入,您可能希望使用'float64(float64[::1],float64[::1])'。 (标量输出,连续输入数组)。如果您使用跨步输入调用显式声明的函数,它将失败,如果您愿意 Numba 完成这项工作,它只会重新编译。 如果不使用fastmath=True,也不能使用 SIMD,因为它会改变结果的精度。

计算至少 4 个部分和(256 位向量),而不是计算这些部分和的总和(Numpy 也不计算天真的总和)。

使用 MSeiferts 优秀基准工具的示例

import numpy as np
import numba as nb
from itertools import chain

def python_loop(a,b):
    result = 0.0
    for i,j in zip(a,b):
        result += (i+j)
    return result

@nb.njit
def numba_loop_zip(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

#Your version with suboptimal input and output (prevent njit compilation) declaration
@nb.jit('float64[:](float64[:],float64[:])')
def numba_your_func(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

@nb.njit(fastmath=True)
def numba_loop_zip_fastmath(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

@nb.njit(fastmath=True)
def numba_loop_fastmath_single(a,b):
    result = 0.0
    size=min(a.shape[0],b.shape[0])
    for i in range(size):
            result += a[i]+b[i]
    return result

@nb.njit(fastmath=True,parallel=True)
def numba_loop_fastmath_multi(a,b):
    result = 0.0
    size=min(a.shape[0],b.shape[0])
    for i in nb.prange(size):
            result += a[i]+b[i]
    return result

#just for fun... single-threaded for small arrays,
#multithreaded for larger arrays
@nb.njit(fastmath=True,parallel=True)
def numba_loop_fastmath_combined(a,b):
    result = 0.0
    size=min(a.shape[0],b.shape[0])
    if size>2*10**4:
        result=numba_loop_fastmath_multi(a,b)
    else:
        result=numba_loop_fastmath_single(a,b)
    return result

def numpy_methods(a, b):
    return a.sum() + b.sum()

def python_sum(a, b):
    return sum(chain(a.tolist(), b.tolist()))

from simple_benchmark import benchmark, MultiArgument

arguments = {
    2**i: MultiArgument([np.zeros(2**i), np.zeros(2**i)])
    for i in range(2, 19)
}
b = benchmark([python_loop, numba_loop_zip, numpy_methods,numba_your_func, python_sum,numba_loop_zip_fastmath,numba_loop_fastmath_single,numba_loop_fastmath_multi,numba_loop_fastmath_combined], arguments, warmups=[numba_loop_zip,numba_loop_zip_fastmath,numba_your_func,numba_loop_fastmath_single,numba_loop_fastmath_multi,numba_loop_fastmath_combined])

%matplotlib notebook
b.plot()

请注意,仅在某些特殊情况下建议使用numba_loop_fastmath_multinumba_loop_fastmath_combined(a,b)。更常见的是,这样一个简单的函数是另一个问题的一部分,可以更有效地并行化(启动线程有一些开销)

【讨论】:

【解决方案3】:

当 numba 生效时,很难划清界限。但是有一些指标可能无效:

  • 如果您不能将 jitnopython=True 一起使用 - 每当您无法在 nopython 模式下编译它时,您要么尝试编译太多,要么编译速度不会明显加快。

  • 如果您不使用数组 - 当您处理传递给 numba 函数的列表或其他类型(从其他 numba 函数除外)时,numba 需要复制这些会导致大量开销。

  • 1234563这些会处理)。

在 numba 比其他解决方案快“一点”的情况下,您可能不想使用它还有另一个原因:在某些情况下,必须提前编译 Numba 函数,或者在第一次调用时编译。即使您调用它数百次,编译也会比您的收益慢得多。编译时间也会增加:numba 导入速度很慢,编译 numba 函数也会增加一些开销。如果导入开销增加 1-10 秒,减少几毫秒是没有意义的。

numba 的安装也很复杂(至少没有 conda),所以如果你想共享你的代码,那么你就有一个真正的“严重依赖”。


您的示例缺少与 NumPy 方法和高度优化的纯 Python 版本的比较。我添加了一些比较函数并做了一个基准测试(使用我的库simple_benchmark):

import numpy as np
import numba as nb
from itertools import chain

def python_loop(a,b):
    result = 0.0
    for i,j in zip(a,b):
        result += (i+j)
    return result

@nb.njit
def numba_loop(a,b):
    result = 0.0
    for i,j in zip(a,b):
            result += (i+j)
    return result

def numpy_methods(a, b):
    return a.sum() + b.sum()

def python_sum(a, b):
    return sum(chain(a.tolist(), b.tolist()))

from simple_benchmark import benchmark, MultiArgument

arguments = {
    2**i: MultiArgument([np.zeros(2**i), np.zeros(2**i)])
    for i in range(2, 17)
}
b = benchmark([python_loop, numba_loop, numpy_methods, python_sum], arguments, warmups=[numba_loop])

%matplotlib notebook
b.plot()

是的,numba 函数对于小数组最快,但是对于较长的数组,NumPy 解决方案会稍微快一些。 Python 解决方案速度较慢,但​​“更快”的替代方案已经比您最初提出的解决方案快得多。

在这种情况下,我会简单地使用 NumPy 解决方案,因为它简短、易读且速度快,除非您处理大量短数组并多次调用该函数 - 那么 numba 解决方案会好得多。

【讨论】:

  • 很好的答案!为什么对于较短的数组,Numba 比 NumPy 快?
  • @Dahn NumPy 函数往往有更多开销,这在短数组上使用它们时会很明显。正如您在图中看到的那样,NumPy 函数对于 1 个元素数组和 100 个元素数组所花费的时间大致相同。这种开销取决于确切的 NumPy 函数,但它通常检查/转换输入参数、覆盖边缘情况和处理数组子类(或可以像数组一样使用的类)。但说白了:这并不重要,因为即使对于短数组,它仍然非常快。
猜你喜欢
  • 2015-06-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-12
  • 1970-01-01
  • 2011-09-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多