为什么向stdout打印如此慢?能加快吗?
我一直对在终端输出内容时,使用打印语句所花费的时间感到惊讶和沮丧。最近因为日志记录太慢,我决定研究一下这个问题,结果发现几乎所有的时间都是在等终端处理结果。
有没有办法加快写入标准输出(stdout)的速度呢?
我写了一个脚本(问题底部的'print_timer.py
')来比较写入10万行到标准输出、文件,以及将标准输出重定向到/dev/null
时的时间。以下是时间结果:
$ python print_timer.py
this is a test
this is a test
<snipped 99997 lines>
this is a test
-----
timing summary (100k lines each)
-----
print :11.950 s
write to file (+ fsync) : 0.122 s
print with stdout = /dev/null : 0.050 s
哇!为了确保Python没有在后台做一些事情,比如识别我把标准输出重定向到/dev/null,我在脚本外面进行了重定向...
$ python print_timer.py > /dev/null
-----
timing summary (100k lines each)
-----
print : 0.053 s
write to file (+fsync) : 0.108 s
print with stdout = /dev/null : 0.045 s
所以这不是Python的技巧,而是终端的问题。我一直知道把输出丢到/dev/null会加快速度,但没想到差别这么大!
让我惊讶的是tty(终端设备)是多么慢。怎么会写入物理磁盘的速度远远快于写入“屏幕”(这应该是全内存操作),而且和直接丢到垃圾的速度差不多?
这个链接讨论了终端如何阻塞输入输出,以便它可以“解析输入,更新帧缓冲,与X服务器通信以滚动窗口等等”……但我不太明白。到底是什么让它这么慢呢?
我想可能没有解决办法(除非有更快的tty实现?),但我还是想问问。
更新:在阅读了一些评论后,我开始想我的屏幕大小对打印时间的影响有多大,结果确实有一些影响。上面那些非常慢的数字是我把Gnome终端放大到1920x1200时的结果。如果我把它缩小到很小,我得到的结果是……
-----
timing summary (100k lines each)
-----
print : 2.920 s
write to file (+fsync) : 0.121 s
print with stdout = /dev/null : 0.048 s
这确实好很多(大约快4倍),但并没有改变我的问题。它只是增加了我的疑问,因为我不明白为什么终端屏幕渲染会拖慢写入标准输出的程序。为什么我的程序需要等屏幕渲染完成才能继续?
所有的终端/tty应用程序都是一样的吗?我还没进行过实验。对我来说,终端应该能够缓存所有传入的数据,隐式解析/渲染,并且只在当前屏幕配置下以合理的帧率渲染最近的可见部分。所以如果我能在大约0.1秒内写入并同步到磁盘,终端应该也能在类似的时间内完成同样的操作(可能在这个过程中更新几次屏幕)。
我仍然希望能有一个tty设置,可以从应用程序端进行更改,以改善程序员的体验。如果这完全是终端应用程序的问题,那这可能就不适合放在StackOverflow上了?
我漏掉了什么吗?
这里是用来生成时间的Python程序:
import time, sys, tty
import os
lineCount = 100000
line = "this is a test"
summary = ""
cmd = "print"
startTime_s = time.time()
for x in range(lineCount):
print line
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)
#Add a newline to match line outputs above...
line += "\n"
cmd = "write to file (+fsync)"
fp = file("out.txt", "w")
startTime_s = time.time()
for x in range(lineCount):
fp.write(line)
os.fsync(fp.fileno())
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)
cmd = "print with stdout = /dev/null"
sys.stdout = file(os.devnull, "w")
startTime_s = time.time()
for x in range(lineCount):
fp.write(line)
t = time.time() - startTime_s
summary += "%-30s:%6.3f s\n" % (cmd, t)
print >> sys.stderr, "-----"
print >> sys.stderr, "timing summary (100k lines each)"
print >> sys.stderr, "-----"
print >> sys.stderr, summary
6 个回答
你的重定向可能没有任何作用,因为程序可以判断它们的输出是否指向一个终端。
当输出指向终端时,标准输出(stdout)很可能是行缓冲的,这和C语言中的stdout
流的行为是一样的。
作为一个有趣的实验,试着把输出通过管道传递给cat
命令。
我也做了一个有趣的实验,下面是结果。
$ python test.py 2>foo
...
$ cat foo
-----
timing summary (100k lines each)
-----
print : 6.040 s
write to file : 0.122 s
print with stdout = /dev/null : 0.121 s
$ python test.py 2>foo |cat
...
$ cat foo
-----
timing summary (100k lines each)
-----
print : 1.024 s
write to file : 0.131 s
print with stdout = /dev/null : 0.122 s
为什么写入物理磁盘的速度比写入“屏幕”(大概是全内存操作)快得多,甚至和直接丢到垃圾里(/dev/null)一样快呢?
恭喜你,你刚刚发现了输入输出缓冲的重要性。:-)
磁盘看起来更快,是因为它有很强的缓冲:所有Python的write()
调用在实际写入物理磁盘之前就已经返回了。(操作系统稍后会处理这些,将成千上万的单独写入合并成一个大块,效率更高。)
而终端几乎没有缓冲:每个单独的print
/ write(line)
都要等到完整写入(也就是显示到输出设备)完成后才能继续。
为了公平比较,你需要让文件测试使用和终端一样的输出缓冲,这可以通过修改你的例子来实现:
fp = file("out.txt", "w", 1) # line-buffered, like stdout
[...]
for x in range(lineCount):
fp.write(line)
os.fsync(fp.fileno()) # wait for the write to actually complete
我在我的机器上运行了你的文件写入测试,使用缓冲时,写入10万行也只需要0.05秒。
但是,按照上面的修改进行无缓冲写入时,写入1000行到磁盘却花了40秒。我等不及写完10万行,但从之前的结果推算,可能需要超过一个小时。
这样一来,终端的11秒就显得很有意义了,是吧?
所以,回答你最初的问题,写入终端实际上是非常快的,考虑到所有因素,提升速度的空间不大(不过不同的终端在处理上会有差异;可以看看Russ对这个回答的评论)。
(你可以增加更多的写入缓冲,就像磁盘I/O那样,但那样你在终端上看到的内容会在缓冲区被刷新后才显示。这是一个权衡:互动性和批量效率之间的选择。)
感谢大家的评论!在你们的帮助下,我最终自己回答了这个问题。不过,自己回答自己的问题感觉有点奇怪。
问题 1:为什么打印到标准输出(stdout)这么慢?
回答:打印到标准输出其实并不慢。慢的是你用的终端。和应用程序那边的输入输出缓冲(比如说 Python 的文件缓冲)几乎没有关系。下面会详细解释。
问题 2:可以加快速度吗?
回答:可以加快,但似乎不能从程序那边(也就是负责“打印”到标准输出的部分)来解决。要加快速度,可以换一个更快的终端模拟器。
解释...
我试了一个自称“轻量级”的终端程序叫 wterm
,结果明显好很多。下面是我在同一台机器上用 wterm
在1920x1200分辨率下运行我的测试脚本(在问题底部有)的输出,而用基本的打印选项在 gnome-terminal 下花了12秒:
----- timing summary (100k lines each) ----- print : 0.261 s write to file (+fsync) : 0.110 s print with stdout = /dev/null : 0.050 s
0.26秒比12秒好太多了!我不知道 wterm
是否在屏幕渲染方面更聪明,像我之前提到的那样(以合理的帧率渲染“可见”的部分),还是说它只是比 gnome-terminal
做得少。无论如何,我的问题有了答案:gnome-terminal
是慢的。
所以,如果你有一个运行时间很长的脚本,感觉很慢,而且输出了大量文本到标准输出... 不妨试试换个终端,看看是否会更好!
需要注意的是,我几乎是随机从 ubuntu/debian 的软件库里找到了 wterm
。这个链接可能是同一个终端,但我不太确定。我没有测试其他的终端模拟器。
更新:因为我想知道,所以我用同一个脚本和全屏(1920x1200)测试了很多其他的终端模拟器。我的手动收集的统计数据在这里:
wterm 0.3s aterm 0.3s rxvt 0.3s mrxvt 0.4s konsole 0.6s yakuake 0.7s lxterminal 7s xterm 9s gnome-terminal 12s xfce4-terminal 12s vala-terminal 18s xvt 48s
记录的时间是手动收集的,但它们相当一致。我记录了最好的(大致)值。显然,你的情况可能会有所不同。
作为额外收获,这也是一次有趣的探索,了解了一些可用的终端模拟器!我很惊讶我第一次尝试的“替代”测试竟然是最好的。