原文: https://bastibe.de/2013-05-30-speeding-up-matplotlib.html
2013年5月30日
郑重声明,Matplotlib棒极了!它的输出看起来令人惊奇,它的可配置性很高,并且使用起来非常简单。得工具如此,夫复何求?
好吧…速度。如果我可以对Matplotlib的一点进行批评的话,那就是它是相对缓慢的。为了测量这种缓慢,我们来做一个非常简单的线性图表,然后尽可能快地在上面画一些随机数。
import matplotlib.pyplot as plt
import numpy as np
import time
fig, ax = plt.subplots()
tstart = time.time()
num_plots = 0
while time.time()-tstart < 1:
ax.clear()
ax.plot(np.random.randn(100))
plt.pause(0.001)
num_plots += 1
print(num_plots)
在我的机子上,我每秒大概可以绘制11个图表。在这里我用 pause()
来更新图表以避免阻塞。正确的做法其实是用 draw()
函数来替代更新。 但是由于 Qt4Agg后端的一个bug,你不能在这儿使用它。如果你没在用Qt4Agg后端,draw()
可能是正确的选择。
对于单张图表,十个图表每秒并不糟糕。但是,这是最简单的情况,所以在这种最简情况下10帧每秒可能对于一些复杂的情况来说就是一件坏事了。
在这个过程中大量耗时的一件事是不断地重复创建所有的轴线和文字标签。所以我们尝试一下不这样做。
除了在每一帧接连调用clear()
和plot()
,从而有效地删除图表的所有内容并重建,我们可以保留一个已经存在的图表而只是改动它的数据部分:
fig, ax = plt.subplots()
line, = ax.plot(np.random.randn(100))
tstart = time.time()
num_plots = 0
while time.time()-tstart < 1:
line.set_ydata(np.random.randn(100))
plt.pause(0.001)
num_plots += 1
print(num_plots)
这样可以每秒生成26张图表。对于这样一个简单的改动来说已经不错了。缺点是数轴不会再跟随数据改变而自动缩放了。因此,它们不会再基于数据变动而改变它们的限制了。
对其进行分析产生了一些有趣的结果:
ncalls | tottime | percall | cumtime | percall | filename:lineno(function) |
---|---|---|---|---|---|
15 | 0.167 | 0.011 | 0.167 | 0.011 | {built-in method sleep) |
在所有事情中用了用了最大块运行时的函数是sleep()
。显然这不是我们想要的。深入探究分析器可以看出这实际上发生在pause()
的调用过程中。于是我再一次思考使用pause
是否是对性能有益的好想法。
正如结果所示,pause()
在内部调用了fig.canvas.draw()
和plt.show()
,然后调用了fig.canvas.start_event_loop()
。fig.canvas.start_event_loop()
的默认实现进而调用了fig.canvas.flush_events()
,然后休眠了所请求的时间。雪上加霜的是,它甚至坚持睡眠至少百分之一秒,这很好地解释了分析器输出中15次call中的0.167秒的sleep()
时间。
将这些方法放在一块得到了
fig, ax = plt.subplots()
line, = ax.plot(np.random.randn(100))
tstart = time.time()
num_plots = 0
while time.time()-tstart < 1:
line.set_ydata(np.random.randn(100))
fig.canvas.draw()
fig.canvas.flush_events()
num_plots += 1
print(num_plots)
到现在可以每秒绘制40帧图表了。需要注意之前提到的show()
的调用因为图表已经在屏幕上了而可以被忽略。flush_events()
只是在跑Qt的事件循环,所以这里应该没有什么可以优化的。
现在唯一可以优化的东西只剩下了fig.canvas.show()
。这个函数实际上是把ax包括的所有元素绘制出来。这些元素可以通过ax.get_children()
获得。对于这样一个简单的图表,有这些元素:
- the background ax.patch
- the line,由
plot()
功能返回的 - the spines ax.spines
- the axes ax.xaxis and ax.yaxis
我们可以在这里做的是部分绘制实际变化了的图表部分。这部分至少是背景和线条。只重绘这部分内容的代码如下:
fig, ax = plt.subplots()
line, = ax.plot(np.random.randn(100))
plt.show(block=False)
tstart = time.time()
num_plots = 0
while time.time()-tstart < 5:
line.set_ydata(np.random.randn(100))
ax.draw_artist(ax.patch)
ax.draw_artist(line)
fig.canvas.update()
fig.canvas.flush_events()
num_plots += 1
print(num_plots/5)
需要注意的是你必须添加fig.canvas.update()
来复制新渲染的线条到绘图后端。
现在每秒可以绘制500帧图表了。五百次每秒!坦率地说,这真是太不可以思议了!
注意到自从我们只重绘背景和线条开始,一些数轴的细节可能会被覆盖。为了把spines也画上,使用spines里的ax.spines.values()
: ax.draw_artist(ax.yaxis)
。如果你将它们都绘制出来,你可以得到近似fig.canvas.draw()
的性能。尤其是数轴的绘制十分消耗性能。
还有一种方法,就是绘制一次完整的图表然后把完整的空背景复制出来,然后复原它并切只在它的顶层绘制一条新的线。这和上面的代码一样快并且没有任何图像失真,但是如果你改变图表的大小就会失效。
总得来说,Matplotlib的灵活性给我留下了深刻的印象。默认情况下,Matplotlib比起性能更注重质量。但是因为它非常灵活可控,如果你在某些地方确实需要高性能,它可以让你随心所欲地调整它。真的是一项惊人的技术啊!
EDIT: 如结果所示,fig.canvas.blit(ax.bbox)
因为疯狂地泄漏内存而成为了一个坏主意。你应该用fig.canvas.update()
代替它,从而拥有相同的高速度但是没有内存泄漏。