为什么向stdout打印如此慢?能加快吗?

202 投票
6 回答
81748 浏览
提问于 2025-04-16 04:59

我一直对在终端输出内容时,使用打印语句所花费的时间感到惊讶和沮丧。最近因为日志记录太慢,我决定研究一下这个问题,结果发现几乎所有的时间都是在等终端处理结果。

有没有办法加快写入标准输出(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 个回答

14

你的重定向可能没有任何作用,因为程序可以判断它们的输出是否指向一个终端。

当输出指向终端时,标准输出(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
190

为什么写入物理磁盘的速度比写入“屏幕”(大概是全内存操作)快得多,甚至和直接丢到垃圾里(/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那样,但那样你在终端上看到的内容会在缓冲区被刷新后才显示。这是一个权衡:互动性和批量效率之间的选择。)

108

感谢大家的评论!在你们的帮助下,我最终自己回答了这个问题。不过,自己回答自己的问题感觉有点奇怪。

问题 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

记录的时间是手动收集的,但它们相当一致。我记录了最好的(大致)值。显然,你的情况可能会有所不同。

作为额外收获,这也是一次有趣的探索,了解了一些可用的终端模拟器!我很惊讶我第一次尝试的“替代”测试竟然是最好的。

撰写回答