将 line_profiler 与 numba jitted 函数一起使用

Using line_profiler with numba jitted functions

是否可以将 line_profiler 与 Numba 一起使用?

在用 @numba.jit returns 修饰的函数上调用 %lprun 一个空配置文件:

Timer unit: 1e-06 s

Total time: 0 s
File: <ipython-input-29-486f0a3cdf73>
Function: conv at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           @numba.jit
     2                                           def conv(f, w):
     3                                               f_full = np.zeros(np.int(f.size + (2 * w.size) - 2), dtype=np.float64)
     4                                               for i in range(0, f_full.size):
     5                                                   if i >= w.size - 1 and i < w.size + f.size - 1:
     6                                                       f_full[i] = f[i - w.size + 1]
     7                                               w = w[::-1]
     8                                               g = np.zeros(f_full.size-w.size + 1, dtype=np.float64)
     9                                               for i in range(0, f_full.size - w.size):
    10                                                   g[i] = np.sum(np.multiply(f_full[i:i+w.size], w))
    11                                               return g

Cython 代码有一个解决方法,但找不到任何 Numba 代码。

TL;DR:可能(技术上)不可能对 numba 函数进行行分析,但即使可以对 numba 函数进行行分析,结果 可能 也不是准确。

分析器和 compiled/optimized 语言的问题

使用 "compiled" 语言的分析器很复杂(甚至在一定程度上使用非编译语言,这取决于运行时允许做什么),因为允许编译器重写您的代码。仅举几个例子:constant folding, inline function calls, unroll loops (to take advantage of SIMD instructions), hoisting, and generally reorder/rearrange expressions (even over multiple lines). Generally the compiler is allowed to do anything as long as the result and the side-effects are "as if" 函数不是 "optimized".

原理图:

+---------------+       +-------------+      +----------+
|  Source file  |   ->  |  Optimizer  |  ->  |  Result  |
+---------------+       +-------------+      +----------+

这是个问题,因为探查器需要在代码中插入语句,例如,函数探查器可能会在每个函数的开头和开头插入一条语句,即使代码经过优化并且函数也可能会起作用是内联的——仅仅因为 "profiler statements" 也是内联的。但是,如果编译器决定 内联一个函数,因为附加的分析器语句怎么办?那么您的配置文件实际上可能与 "real program" 的执行方式不同。

例如,如果你有(我在这里使用 Python 即使它没有编译,假设我用 C 左右编写了这样的程序):

 def give_me_ten():
     return 10

 def main():
     n = give_me_ten()
     ...

然后优化器可以将其重写为:

 def main():
     n = 10  # <-- inline the function

但是,如果您插入探查器语句:

 def give_me_ten():
     profile_start('give_me_ten')
     n = 10
     profile_end('give_me_ten')
     return n

 def main():
     profile_start('main')
     n = give_me_ten()
     ...
     profile_end('main')

优化器可能只是发出相同的代码,因为它没有内联函数。

行分析器实际上会在您的代码中插入更多 "profiler statements"。在每一行的开头和结尾。这可能会阻止很多编译器优化。我不太熟悉 "as-if" 规则,但我的猜测是那时不可能进行很多优化。因此,带有分析器的编译程序与没有分析器的编译程序的行为会有很大不同。

例如,如果您有这个程序:

 def main():
     n = 1
     for _ in range(1000):
         n += 1
     ...

优化器可以(不确定是否有编译器会这样做)将其重写为:

 def main():
     n = 1001  # all statements are compile-time constants and no side-effects visible

但是,如果您有行分析语句,则:

 def main():
     profile_start('main', line=1)
     n = 1
     profile_end('main', line=1)
     profile_start('main', line=2)
     for _ in range(1000):
         profile_end('main', line=2)
         profile_start('main', line=3)
         n += 1
         profile_end('main', line=3)
         profile_start('main', line=2)
     ...

然后根据"as-if"规则,循环有副作用,不能压缩为单个语句(也许代码仍然可以优化,但不能作为单个语句)。

请注意,这些都是简单的示例,compilers/optimizers 通常非常复杂,并且有 很多 可能的优化。

根据语言、编译器和分析器的不同,可能可以减轻这些影响。但是面向 Python 的分析器(例如行分析器)不太可能针对 C/C++ 编译器。

另请注意,Python 并不是一个真正的问题,因为 Python 只是一步一步地执行一个程序(不是真的,但 Python 很少,很少改变你的"written code" 然后只是在次要方面)。

这如何适用于 Numba 和 Cython?

  • Cython 将您的 Python 代码翻译成 C(或 C++)代码,然后使用 C(或 C++)编译器对其进行编译。原理图:

    +-------------+    +--------+    +----------+    +-----------+    +--------+
    | Source file | -> | Cython | -> | C source | -> | Optimizer | -> | Result |
    +-------------+    +--------+    +----------+    +-----------+    +--------+
    
  • Numba 根据参数类型翻译您的 Python 代码,并使用 LLVM 编译代码。原理图:

    +-------------+    +-------+    +------------------+    +--------+
    | Source file | -> | Numba | -> | LLVM / Optimizer | -> | Result |
    +-------------+    +-------+    +------------------+    +--------+
    

两者都有一个可以进行广泛优化的编译器。如果在编译之前将性能分析语句插入到代码中,则许多优化将无法实现。因此,即使可以对代码进行行分析,结果也可能不准确(准确的意思是实际程序会以这种方式执行)。

Line-profiler 是为纯 Python 编写的,所以如果它有效,我不一定会相信 Cython/Numba 的输出。它可能会给出一些提示,但总的来说它可能太不准确了。

尤其是 Numba 可能真的很棘手,因为 numba 翻译器需要支持分析语句(否则你最终会得到一个对象模式的 numba 函数,它会产生完全不准确的结果) 你的 jitted 函数不再只是一个函数了。它实际上是一个调度程序,根据参数的类型委托给 "hidden" 函数。因此,当您使用 intfloat 调用相同的 "dispatcher" 时,它可能会执行完全不同的功能。有趣的事实:使用函数分析器进行分析的行为已经产生了显着的开销,因为 numba 开发人员希望实现这一点(参见 )。

好的,如何介绍他们?

您可能应该使用可以在已翻译代码 上与编译器一起工作的分析器进行分析。这些可以(可能)产生比为 Python 代码编写的探查器更准确的结果。这将更加复杂,因为这些分析器将为 翻译代码 生成 return 结果,必须再次手动将其传输到 原始代码 .此外,它甚至可能是不可能的——通常 Cython/Numba 管理结果的翻译、编译和执行,因此您需要检查它们是否为附加分析器提供挂钩。我没有这方面的经验。

作为一般规则:如果您有优化器,那么 总是 将分析视为 "guide" 而不是 "fact"。并且始终使用专为 compiler/optimizer 设计的分析器,否则您将失去很多可靠性 and/or 准确性。