TL;DR:对 numba 函数进行线分析可能(技术上)不可能,但即使可以对 numba 函数进行线分析,结果也可能不准确。
分析器和编译/优化语言的问题
将分析器与“编译”语言一起使用是很复杂的(甚至在某种程度上使用非编译语言,这取决于运行时允许做什么),因为允许编译器重写您的代码。仅举几个例子:常量折叠、内联函数调用、展开循环(利用SIMD 指令)、提升以及通常重新排序/重新排列表达式(甚至多行)。通常,只要结果和副作用“好像”该函数没有“优化”,编译器就可以做任何事情。
示意图:
+---------------+ +-------------+ +----------+
| Source file | -> | Optimizer | -> | Result |
+---------------+ +-------------+ +----------+
这是一个问题,因为探查器需要在代码中插入语句,例如,函数探查器可能会在每个函数的开头和开头插入一条语句,即使代码经过优化并且函数是内联的,这也可能起作用 - 仅仅是因为“分析器语句”也是内联的。但是,如果编译器因为附加的分析器语句而决定不内联函数怎么办?那么您所描述的内容实际上可能与“真实程序”的执行方式不同。
例如,如果你有(我在这里使用 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')
优化器可能只是发出相同的代码,因为它没有内联函数。
行探查器实际上在您的代码中插入了更多“探查器语句”。在每行的开头和结尾。这可能会阻止很多编译器优化。我对“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”规则,循环具有副作用并且不能被压缩为单个语句(也许代码仍然可以优化但不能作为单个语句)。
请注意,这些都是简单的示例,编译器/优化器通常非常复杂并且有很多可能的优化。
根据语言、编译器和分析器的不同,可能会减轻这些影响。但是面向 Python 的分析器(例如 line-profiler)不太可能以 C/C++ 编译器为目标。
另请注意,这不是 Python 的真正问题,因为 Python 只是一步一步地执行程序(不是真的,但 Python 非常、非常少地更改您的“书面代码”,然后只是以较小的方式)。
这如何适用于 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 函数不再只是一个函数。它实际上是一个调度程序,根据参数的类型委托给“隐藏”函数。int
因此,当您使用 an或 a调用相同的“调度程序”时,float
它可以执行完全不同的功能。有趣的事实:使用函数探查器进行分析的行为已经产生了很大的开销,因为 numba 开发人员希望让这项工作正常工作(请参阅cProfile 在调用 numba jit 函数时增加了显着的开销)。
好的,如何描述它们?
您可能应该使用可以与编译器一起处理已翻译代码的分析器进行分析。这些可以(可能)产生比为 Python 代码编写的分析器更准确的结果。这将更加复杂,因为这些分析器将返回翻译代码的结果,这些结果必须再次手动传输到原始代码。此外,它甚至可能都不可能 - 通常 Cython/Numba 管理结果的翻译、编译和执行,因此您需要检查它们是否为附加分析器提供挂钩。我在那里没有经验。
作为一般规则:如果您有优化器,则始终将分析视为“指南”,而不必视为“事实”。并且始终使用为编译器/优化器设计的分析器,否则您将失去很多可靠性和/或准确性。