TL;DR:对 numba 函数进行线分析可能(技术上)不可能,但即使可以对 numba 函数进行线分析,结果可能也不准确。
分析器和编译/优化语言的问题
将分析器与“编译”语言一起使用是很复杂的(甚至在某种程度上使用非编译语言,这取决于运行时允许做什么),因为允许编译器重写您的代码。仅举几个例子:constant folding、inline function calls、unroll loops(利用SIMD instructions)、hoisting,以及通常重新排序/重新排列表达式(甚至超过多行)。一般来说,只要结果和副作用是"as if" 函数没有“优化”,编译器就可以做任何事情。
示意图:
+---------------+ +-------------+ +----------+
| 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 或float 调用相同的“调度程序”时,它可以执行完全不同的功能。有趣的事实:使用函数探查器进行分析的行为已经产生了巨大的开销,因为 numba 开发人员想要完成这项工作(请参阅cProfile adds significant overhead when calling numba jit functions)。
好的,如何分析它们?
您可能应该使用可以在翻译代码上与编译器一起使用的分析器进行分析。这些可以(可能)产生比为 Python 代码编写的分析器更准确的结果。这将更加复杂,因为这些分析器将返回翻译代码的结果,这些结果必须再次手动传输到原始代码。此外,它甚至可能都不可能 - 通常 Cython/Numba 管理结果的翻译、编译和执行,因此您需要检查它们是否为附加分析器提供挂钩。我在那里没有经验。
并且作为一般规则:如果您有优化器,那么总是将分析视为“指南”而不一定是“事实”。并且始终使用专为编译器/优化器设计的分析器,否则您将失去很多可靠性和/或准确性。