【问题标题】:Performance loss in numba compiled logic comparisonnumba 编译逻辑比较中的性能损失
【发布时间】:2021-05-14 04:43:31
【问题描述】:

以下用于逻辑比较的 numba 编译函数中性能下降的原因可能是什么:

from numba import njit

t = (True, 'and_', False)

#@njit(boolean(boolean, unicode_type, boolean))    
@njit
def f(a,b,c):
    if b == 'and_':
        out = a&c
    elif b == 'or_':
        out = a|c
    return out
x = f(*t)
%timeit f(*t)
#1.78 µs ± 9.52 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit f.py_func(*t)
#108 ns ± 0.0042 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

按照答案中的建议进行大规模测试:

x = np.random.choice([True,False], 1000000)
y = np.random.choice(["and_","or_"], 1000000)
z = np.random.choice([False, True], 1000000)

#using jit compiled f
def f2(x,y,z):
    L = x.shape[0]
    out = np.empty(L)
    for i in range(L):
        out[i] = f(x[i],y[i],z[i])
    return out

%timeit f2(x,y,z)
#2.79 s ± 86.4 ms per loop

#using pure Python f
def f3(x,y,z):
    L = x.shape[0]
    out = np.empty(L)
    for i in range(L):
        out[i] = f.py_func(x[i],y[i],z[i])
    return out

%timeit f3(x,y,z)
#572 ms ± 24.3 ms per

我是否遗漏了什么,是否有办法编译“快速”版本,因为这将成为循环执行 ~ 1e6 次的一部分。

【问题讨论】:

  • 1.您始终可以在 c 中实现并调用 dll。 2.我会预先将“and_”和“or_”标记化(也许这可以向量化),并根据标记调用正确的函数,从而跳过if-elif语句。
  • 这听起来像是一个简单的编译器版本。我支持首先标记化的建议,一旦你有标记化的代码,当标记类型已知时,对每个标记进行操作。例如,如果您有一个“And”标记和一个“Or”标记,两者都使用.op() 方法,您将能够先标记化然后.op(),这将永远不会执行if-else。这是一个 2-pass 算法。
  • 如果这必须是一个单通道算法,我相信它可以在某种前瞻的帮助下完成。无论如何,如果这对性能要求很高,省去你的麻烦,用 c 或 c++ 编写,只从 Python 调用
  • 地图don't work。两个版本都有if/else
  • 您是否尝试定义继承结构,例如BaseOperator(Object), def op(*args)And(BaseOperator) Or(BaseOperator)?然后定义静态类型。

标签: python performance compiler-construction numba


【解决方案1】:

您的工作粒度太小。 Numba 不是为此而设计的。您看到的几乎所有执行时间都来自包装/展开参数、类型检查、Python 函数包装、引用计数等的开销。此外,使用 Numba 的好处在这里非常小,因为 Numba 几乎没有优化unicode 字符串操作。

检查这个假设的一种方法是执行以下简单的函数:

@njit
def f(a,b,c):
    return a
x = f(True, 'and_', False)
%timeit f(True, 'and_', False)

普通函数和原始版本在我的机器上都需要 1.34 µs。

此外,您可以反汇编 Numba 函数,以查看执行了多少指令来执行一次调用,并深入了解开销的来源。

如果您希望 Numba 有用,您需要在编译后的函数中添加更多工作,可能通过直接在数组/列表上工作。如果由于输入类型的动态特性而无法做到这一点,那么 Numpy 可能不是这里的正确工具。您可以尝试修改您的代码并改用 PyPy编写本机 C/C++ 模块可能会有所帮助,但大部分时间将花在操作动态对象和 unicode 字符串以及进行类型自省上,除非你重写整个代码。


更新

仅在从 Python 类型转换到 Numba 时(反之亦然)才会支付上述开销。您可以通过以下基准看到这一点:

@njit
def f(a,b,c):
    if b == 'and_':
        out = a&c
    elif b == 'or_':
        out = a|c
    return out
@jit
def manyCalls(a, b, c):
    res = True
    for i in range(1_000_000):
        res ^= f(a, b, c ^ res)
    return res

t = (True, 'and_', False)
x = manyCalls(*t)
%timeit manyCalls(*t)

在我的机器上调用 manyCalls 需要 3.62 毫秒。这意味着对f 的每次调用平均需要 3.6 ns(16 个周期)。这意味着开销只支付一次(当manyCalls 被调用时)。

【讨论】:

  • 感谢您的宝贵时间!毫无疑问,numba 会在冗长的矢量化循环上大放异彩。问题是如何正确编译将被送入循环的逻辑解析器,是否有可能恢复 10 倍的性能损失(归根结底将 numba 提供净收益)。
  • 问题不在于 10 倍的性能损失。如果解析代码也是 JITed,您可以轻松删除它。主要问题是 Numba 是否真的可以提高解析代码的性能,我不确定这是否可能,主要是因为输入本身:动态类型和递归对 Numba 来说真的很糟糕。 Numba 在这里可以提供帮助的唯一情况是表达式 t 对于许多不同的 X 是相同的。在这种情况下,您可以专门为给定的t 生成编译函数。否则,我的建议是使用其他工具或用 C/C++ 重写解析器。
  • 我不是在 10 条语句的逻辑上进行性能改进。 10 倍减速乘以 1e6 让我感到震惊。
  • 您可以将 Numba 开销视为 CPython 和 Numba 之间要支付的固定转换成本。 AFAIK,当调用者也是(njitted)Numba 函数时,不应为调用(njitted)Numba 函数支付此费用。 1.34 µs * 1e6 = 1.34 s 是个问题,但支付 1.34 µs 一次应该没问题。所以这就是为什么 _eval 函数应该使用 Numba 以及可能调用 _eval 的代码。
  • 我也更新了我的问题,两个更新都告诉我:在 Pyhton 和 numba 编译代码之间来回发送数据是浪费时间。
猜你喜欢
  • 2018-03-24
  • 2015-06-15
  • 2018-09-13
  • 1970-01-01
  • 1970-01-01
  • 2018-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多