简述:对于numba函数进行行级别的性能分析可能不可行,即使可以对其进行行级别性能分析,结果也可能不准确。
性能分析工具和编译/优化语言的问题
使用性能分析工具对于“编译型”语言来说非常复杂(即使对于非编译型语言,取决于运行时允许做什么),因为编译器允许重写您的代码。举几个例子:
常量折叠,
内联函数调用,
展开循环(以利用
SIMD指令),
提升,并通常重新排序/重新排列表达式(甚至跨多行)。通常情况下,只要结果和副作用
"好像"函数没有被“优化”,编译器就可以任意操作。
示意图:
+---------------+ +-------------+ +----------+
| Source file | -> | Optimizer | -> | Result |
+---------------+ +-------------+ +----------+
这是一个问题,因为分析器需要在代码中插入语句,例如函数分析器可能会在每个函数的开头和结尾插入一条语句,即使代码被优化并且函数被内联,也可以工作-因为“分析器语句”也被内联了。但是,如果编译器决定
不内联函数,因为有额外的分析器语句怎么办?那么你所分析的实际上可能与“真正的程序”执行的方式不同。
例如,如果你有以下代码(我在这里使用Python,即使它没有编译,只需假设我写了这样的程序):
def give_me_ten():
return 10
def main():
n = give_me_ten()
...
然后优化器可以将其重写为:
def main():
n = 10
然而,如果您插入分析器语句:
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
然而,如果您有行性能分析语句,则:
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)
...
根据“仿佛”规则,循环具有副作用,不能被压缩为单个语句(也许代码仍然可以优化,但不是作为单个语句)。请注意,这些都是简单的例子,编译器/优化器通常非常复杂,并且有大量可能的优化。根据语言、编译器和分析器的不同,可能有可能减轻这些影响。但是像 line-profiler 这样的 Python 定向分析器不太可能针对 C/C++ 编译器。此外,请注意,这并不是 Python 的实际问题,因为 Python 只是逐步执行程序(不完全正确,但 Python 很少以次要方式更改您的“编写代码”)。Numba 和 Cython 如何应用这个呢?
Cython translates your Python code into C (or C++) code and then uses a C (or C++) compiler to compile it. Schematic:
+-------------+ +--------+ +----------+ +-----------+ +--------+
| Source file | -> | Cython | -> | C source | -> | Optimizer | -> | Result |
+-------------+ +--------+ +----------+ +-----------+ +--------+
Numba translates your Python code depending on the argument types and uses LLVM to compile the code. Schematic:
+-------------+ +-------+ +------------------+ +--------+
| Source file | -> | Numba | -> | LLVM / Optimizer | -> | Result |
+-------------+ +-------+ +------------------+ +--------+
两者都有编译器,可以进行广泛的优化。如果在编译代码之前插入分析语句,则许多优化将不可能。因此,即使可以对代码进行逐行分析,结果也可能不准确(准确意味着真正的程序执行方式)。
Line-profiler是为纯Python编写的,因此如果它能工作,我不一定会信任Cython / Numba的输出。它可能会给出一些提示,但总体而言可能太不准确了。
特别是Numba可能非常棘手,因为numba翻译器需要支持分析语句(否则您会得到一个对象模式numba函数,这将产生完全不准确的结果),而且您的jitted函数不仅仅是一个函数。它实际上是一个调度程序,根据参数类型委派到“隐藏”的函数。因此,当您使用
int
或
float
调用相同的“dispatcher”时,它可能会执行完全不同的函数。有趣的事实:使用函数分析器进行分析已经会产生显着的开销,因为numba开发人员想让其工作(请参见
cProfile adds significant overhead when calling numba jit functions)。
好的,如何对它们进行性能分析?
你应该使用一个可以与翻译后的代码一起工作的性能分析器来进行分析。这些性能分析器(可能)会比针对Python代码编写的分析器产生更准确的结果。但是这将会更加复杂,因为这些性能分析器将返回针对翻译后的代码的结果,这些结果必须手动转移到原始代码中。而且这可能甚至不可能 - 通常Cython/Numba管理结果的翻译、编译和执行,因此你需要检查它们是否提供了附加性能分析器的挂接点。我在那方面没有经验。
并且作为一般规则:如果你有优化器,那么始终将性能分析视为“指南”,而不一定是“事实”。并且始终使用专为编译器/优化器设计的性能分析器,否则你将失去很多可靠性和/或准确性。