本文介绍一下SCALENE的实现原理。SCALENE是一个专门为Python设计的分析器,能够同时分析CPU时间、内存使用情况和GPU使用情况,并提供细粒度(行粒度)的性能信息。
总之,按照文章的说明,它比之前的分析器都NB。
文章:OSDI 2023: Triangulating Python Performance Issues with Scalene
CPU 分析
传统方法
周期性地中断程序执行并检查当前的程序计数器(PC)。时间可以按照现实时间(CPU时间加IO等时间)计算,也可按照CPU时间(虚拟时间)计算。
原理:大样本情况下,PC所在进程的次数与进程执行时间成正比。
局限1:传统方法不能有效处理Python-本机代码交错执行的性能分析。在解释器没有获得控制权时,无法处理中断,所以执行本机代码时无法通过定时器中断进行抽样分析。
局限2:使用抽样方法难以分析子线程的执行时间。Python的信号由主线程处理,所以抽样仅对主线程计时,无法获得子线程的运行时间。
本文贡献
技术1:精确的Python-本机代码交错执行计时
通过信号传递判断当前正在运行Python代码还是本机代码。如果中断信号被延迟处理,则正在运行本机代码,如Figure1所示。在两次中断之间,通过虚拟时间time.process_time()之差得到本机代码的执行时间。在中断到来时,通过遍历Python堆栈来寻找当前正在执行的代码行,从而对运行时间归因,提供细粒度的分析结果。
技术2:精确的子线程计时
- 重新定义主线程的阻塞行为,通过使用猴子补丁的方式将threading.join等阻塞函数修改为循环等待函数。从而保证主线程能够一直处理定时器中断。
- 为每个线程维护一个状态标志,主线程处理信号时,遍历所有线程,找到当前正在执行的线程,并获取其调用栈。
- 通过字节码反汇编,通过CALL(CALL, CALL_FUNCTION, CALL_METHOD)来分析当前执行的代码行。如果执行Python代码,则很快就会进入跳转目标,否则若执行本机代码,则会一直停留在CALL指令上。
内存分析
传统方法
Rate-Based Sampling:基于抽样,抽样的次数正比于分配或释放的内存数量,例如每分配或释放512KB采样一次。
局限:无法帮助定位内存泄漏。尽管可以记录内存分配的情况,以及被哪些代码占用了,可能存在内存问题,但不能说明这是内存泄漏。
本文方法
原理:使用基于阈值的(Threshold-Based)采样而不是基于分配数量的采样。当内存的改变量大于设定阈值(|Alloca-Free|>Threshold)时,进行采样操作。
优势:降低了采样次数,并减少了运行时开销。如Figure2所示。
方法:
- 拦截了系统内存分配器调用和Python的内置内存分配器,并替换为自己的shim分配器使用,已有代码改写系统分配器调用,并通过LD_PRELOAD(Linux)或者DYLD_INSERT_LIBRARIES(Mac OS X)注入。对于Python内部的分配器,使用Python的自定义API修改实现
- 在基于阈值的采样触发时,追踪产生申请或释放内存的语句,并通过后台线程记录统计信息。
- 在采样触发时,统计各个对象分配和释放的记录,并通过Laplace’s Rule of Succession来计算内存泄漏发生的可能性。
GPU 分析
方法 在配备NVIDIA GPU的系统上执行行级GPU利用率和内存分析,每进行一次CPU采样时,就收集当前使用的总GPU内存和利用率,并将其与当前执行的代码行相关联,从而帮助Python程序员确定他们是否有效地利用了GPU。
总结
亮点
本文的亮点是在CPU和内存上的抽样工作。
- 主要是CPU上的抽样,对于原生代码/外部库的运行时间区分以及多线程之间的运行时间区分,通过针对Python解释器的语言特性进行特殊化的处理,有可能可以拓展到其他的脚本/解释型语言上。
- 而对于内存的抽样,则体现在新的抽样策略的改进,并对于之前的分析器难以进行的内存泄漏检测提出了解决方案。
不足
- 对于GPU的分析像是狗尾续貂,强行增加来说明自己工作的NB。
- CPU的抽样只是基于某行代码的运行时间,未必能够表示CPU的实际占用率,例如sleep的执行。
- 内存分析中的泄漏检测是基于概率,还需要进一步地核实是否发生了内存泄漏,即有误报,同时无法证明没有漏报。