为啥打印到标准输出这么慢?可以提速吗?
Posted
技术标签:
【中文标题】为啥打印到标准输出这么慢?可以提速吗?【英文标题】:Why is printing to stdout so slow? Can it be sped up?为什么打印到标准输出这么慢?可以提速吗? 【发布时间】:2011-04-20 21:36:13 【问题描述】:我一直对使用 print 语句简单地输出到终端需要多长时间感到惊讶/沮丧。在最近一些令人痛苦的缓慢记录之后,我决定研究它,并惊讶地发现几乎所有所花费的时间都在等待终端处理结果。
可以以某种方式加快写入标准输出的速度吗?
我写了一个脚本(这个问题底部的'print_timer.py
')来比较将 100k 行写入标准输出、文件以及将标准输出重定向到/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 没有在幕后做一些事情,比如认识到我将 stdout 重新分配给 /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 的速度让我吃惊。为什么写入物理磁盘比写入“屏幕”(可能是全 RAM 操作)快得多,并且实际上与使用 /dev/null 转储到垃圾中一样快?
This link 谈论终端将如何阻止 I/O,以便它可以“解析 [输入]、更新其帧缓冲区、与 X 服务器通信以滚动窗口等等” ...但我不完全明白。什么可以花这么长时间?
我预计没有出路(缺少更快的 tty 实现?)但我还是会问。
更新:在阅读了一些 cmets 之后,我想知道我的屏幕尺寸实际上对打印时间有多大影响,而且它确实具有一定的意义。上面真正慢的数字是我的 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
这当然更好(~4x),但不会改变我的问题。它只是添加到我的问题,因为我不明白为什么终端屏幕渲染应该减慢应用程序写入标准输出的速度。为什么我的程序需要等待屏幕渲染才能继续?
不是所有终端/tty 应用程序都是平等的吗?我还没有做实验。在我看来,终端应该能够缓冲所有传入的数据,不可见地解析/渲染它,并且仅以合理的帧速率渲染当前屏幕配置中可见的最新块。因此,如果我可以在大约 0.1 秒内将 +fsync 写入磁盘,那么终端应该能够以该顺序完成相同的操作(在执行此操作时可能会进行一些屏幕更新)。
我仍然有点希望有一个可以从应用程序端更改的 tty 设置,以使这种行为对程序员更好。如果这严格来说是一个终端应用程序问题,那么这可能甚至不属于 ***?
我错过了什么?
这是用于生成计时的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
【问题讨论】:
写入标准输出的全部目的是让人们可以读取输出。世界上没有人能在 12 秒内读完 10000 行文本,那么让 stdout 更快有什么意义??? @Seun Osewa:一个例子(引发了我的问题)是在做print statement debugging 之类的事情时。您想运行您的程序并在结果发生时查看结果。您显然是对的,大多数行都会飞过而您看不到,但是当发生异常时(或者您点击了您仔细放置的条件 getch/raw_input/sleep 语句),您希望直接查看打印输出而不是经常需要打开或刷新文件视图。 打印语句调试是 tty 设备(即终端)默认使用行缓冲而不是块缓冲的原因之一:如果程序挂起并且最后几行,调试输出没有多大用处的调试输出仍在缓冲区中,而不是刷新到终端。 @Stephen:这就是为什么我没有费心去追求一位评论者声称通过增加缓冲区大小来实现巨大改进的原因。它完全违背了调试打印的目的!我在调查时做了一些实验,但没有看到任何净改善。我仍然对这种差异感到好奇,但不是真的。 @SeunOsewa 不,不,再次不!写入 stdout 的全部目的是它是写入输出的标准位置。不要将标准输出与终端混为一谈。这整个问题都是错误的。写入标准输出不本质上比写入任何其他文件慢。写入终端很慢。 STDOUT 不是终端。原谅我的咆哮,但请让我再说一遍。不要将标准输出与 tty 混为一谈。它们是两个非常不同的东西,只是碰巧经常联系在一起。 【参考方案1】:为什么写入物理磁盘比写入“屏幕”(可能是全 RAM 操作)要快得多,并且实际上与使用 /dev/null 转储到垃圾中一样快?
恭喜,您刚刚发现了 I/O 缓冲的重要性。 :-)
磁盘似乎更快,因为它是高度缓冲的:所有 Python 的 write()
调用都在实际写入物理磁盘之前返回。 (操作系统稍后会这样做,将数千个单独的写入组合成一个大而高效的块。)
另一方面,终端很少或根本不做缓冲:每个人 print
/ write(line)
等待完整写入(即显示到输出设备)完成。
为了使比较公平,您必须使文件 test 使用与终端相同的输出缓冲,您可以通过将示例修改为:
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
我在我的机器上运行了你的文件写入测试,通过缓冲,这里也是 0.05 秒,100,000 行。
但是,通过以上对无缓冲写入的修改,仅将 1,000 行写入磁盘需要 40 秒。我放弃了等待 100,000 行来写,但从前面推断,这将需要 一个多小时。
这让终端机的 11 秒变得清晰起来,不是吗?
因此,要回答您最初的问题,考虑到所有因素,写入终端实际上非常快,并且没有太多空间可以让它更快(但各个终端的工作量确实有所不同;请参阅 Russ 的对此答案发表评论)。
(您可以添加更多的写入缓冲,例如使用磁盘 I/O,但是在刷新缓冲区之前您不会看到写入终端的内容。这是一种权衡:交互性与批量效率。)
【讨论】:
我得到了 I/O 缓冲......你当然提醒我,我应该 fsync'd 来真正比较完成时间(我会更新问题),但是 fsync 每行是精神错乱。 tty 真的需要有效地做到这一点吗?是否没有与文件等效的终端/操作系统端缓冲?即:应用程序在终端呈现到屏幕之前写入标准输出并返回,终端(或操作系统)将其全部缓冲。然后终端可以明智地将尾巴以可见的帧速率渲染到屏幕上。有效地阻止每一行似乎很愚蠢。我觉得我仍然缺少一些东西。 您可以自己打开一个带有大缓冲区的 stdout 句柄,使用类似os.fdopen(sys.stdout.fileno(), 'w', BIGNUM)
的东西。但是,这几乎永远不会有用:几乎所有应用程序都必须记住在用户预期输出的每一行之后显式刷新。
我之前尝试过使用巨大的(使用fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)
时最大 10MB)python 端缓冲区。影响为零。即:仍然很长的 tty 延迟。这让我想到/意识到你只是推迟了缓慢的 tty 问题......当 python 的缓冲区最终刷新时,tty 似乎仍然在返回之前对流进行相同的处理。
请注意,此答案具有误导性和错误性(对不起!)。具体来说,说“没有太多空间让它更快[超过 11 秒]”是错误的。请参阅我自己对问题的回答,其中我显示 wterm 终端在 0.26 秒内达到了相同的 11 秒结果。
Russ:感谢您的反馈!在我这边,更大的 fdopen
缓冲区 (2MB) 肯定会产生巨大的影响:它将打印时间从几秒减少到 0.05 秒,与文件输出相同(使用 gnome-terminal
)。【参考方案2】:
感谢所有cmets!在您的帮助下,我最终自己回答了这个问题。不过,回答自己的问题感觉很脏。
问题 1:为什么打印到标准输出很慢?
答案:打印到标准输出并不本身就很慢。您使用的终端很慢。它与应用程序端的 I/O 缓冲几乎为零(例如:python 文件缓冲)。见下文。
问题 2:可以提速吗?
答案:可以,但似乎不是从程序方面(对标准输出进行“打印”的一方)。要加快速度,请使用更快的不同终端仿真器。
解释...
我尝试了一个名为wterm
的自称为“轻量级”终端程序,并获得了显着更好的结果。下面是我的测试脚本的输出(在问题的底部),在 wterm
以 1920x1200 在同一系统上运行时,基本打印选项使用 gnome-terminal 需要 12 秒:
0.26s 比 12s 好很多!我不知道wterm
是否更智能地按照我的建议(以合理的帧速率渲染“可见”尾部)来渲染屏幕,或者它是否比@“做得更少” 987654325@。不过,就我的问题而言,我已经得到了答案。 gnome-terminal
很慢。
所以 - 如果您有一个长时间运行的脚本,您觉得它很慢并且它会向标准输出发送大量文本...尝试不同的终端,看看它是否更好!
请注意,我几乎从 ubuntu/debian 存储库中随机提取了 wterm
。 This link 可能是同一个终端,但我不确定。我没有测试任何其他终端模拟器。
更新:因为我不得不抓痒,我用相同的脚本和全屏 (1920x1200) 测试了一大堆其他终端模拟器。我手动收集的统计数据在这里:
wterm 0.3s aterm 0.3s rxvt 0.3s mrxvt 0.4s 控制台0.6s yakuake 0.7s lxterminal 7s xterm 9s gnome 终端 12s xfce4-终端 12s vala-terminal 18s xvt 48s记录的时间是手动收集的,但它们非常一致。我记录了最好的(ish)价值。显然是 YMMV。
作为奖励,这是对一些可用的各种终端仿真器的有趣之旅!我很惊讶我的第一个“替代”测试竟然是最好的。
【讨论】:
你也可以试试 aterm。这是我使用您的脚本进行的测试的结果。 Aterm - 打印:0.491 秒,写入文件(+fsync):0.110 秒,使用标准输出打印 = /dev/null:0.087 秒 wterm - 打印:0.521 秒,写入文件(+fsync):0.105 秒,使用标准输出打印= /dev/null : 0.085 秒 urxvt 与 rxvt 相比如何? 另外,screen
,(程序)应该包含在列表中! (或byobu
,它是screen
的包装器,具有增强功能)此实用程序允许有多个终端,就像X 终端中的选项卡一样。我假设打印到当前screen
的终端与打印到普通终端相同,但是在screen
的一个终端中打印然后切换到另一个没有活动的终端呢?
很奇怪,前段时间我在比较不同的终端的速度,而 gnome-terminal 在相当严肃的测试中表现最好,而 xterm 最慢。也许从那时起他们就一直在努力缓冲。对 unicode 的支持也有很大的不同。
OSX 上的 iTerm2 给了我:print: 0.587 s, write to file (+fsync): 0.034 s, print with stdout = /dev/null : 0.041 s
。在 iTerm2 中运行“屏幕”:print: 1.286 s, write to file (+fsync): 0.043 s, print with stdout = /dev/null : 0.033 s
【参考方案3】:
除了输出可能默认为行缓冲模式外,输出到终端还会导致您的数据流入具有最大吞吐量的终端和串行线路,或伪终端和单独的进程,即处理显示事件循环,从某种字体渲染字符,移动显示位以实现滚动显示。后一种情况可能分布在多个进程上(例如 telnet 服务器/客户端、终端应用程序、X11 显示服务器),因此也存在上下文切换和延迟问题。
【讨论】:
真的!这促使我尝试将我的终端窗口大小(在 Gnome 中)减小到一些微不足道的东西(从 1920x1200 开始)。果然... 2.8s 打印时间 vs 11.5s。好多了,但仍然......为什么它停滞不前?您会认为标准输出缓冲区 (hmm) 可以处理所有 100k 行,而终端显示器只会从缓冲区的尾端抓取它可以在屏幕上显示的任何内容,然后快速完成。 如果 xterm(或 gterm,在这种情况下)认为它不必同时显示所有其他输出,它将更快地呈现最终屏幕。如果它试图走这条路,它可能会使小屏幕更新的常见情况看起来反应迟钝。在编写这种类型的软件时,您有时可以通过使用不同的模式并尝试检测何时需要从小型操作模式转换为批量操作模式来处理它。您可以经常使用cat big_file | tail
甚至cat big_file | tee big_file.cpy | tail
来加快速度。【参考方案4】:
您的重定向可能没有任何作用,因为程序可以确定它们的输出 FD 是否指向 tty。
当指向终端时,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
【讨论】:
我没想到python会检查它的输出FS。我想知道python是否在幕后耍花招?我不希望,但不知道。 +1 指出缓冲中最重要的区别 @Russ:-u
选项强制 stdin
、stdout
和 stderr
不缓冲,这将比块缓冲慢(由于开销)【参考方案5】:
我不能谈论技术细节,因为我不了解它们,但这并不让我感到惊讶:终端不是为打印大量这样的数据而设计的。事实上,您甚至提供了一个指向大量 GUI 内容的链接,每次您想要打印某些东西时它都必须这样做!请注意,如果您改为使用pythonw
调用脚本,则不需要15 秒;这完全是一个 GUI 问题。将stdout
重定向到一个文件以避免这种情况:
import contextlib, io
@contextlib.contextmanager
def redirect_stdout(stream):
import sys
sys.stdout = stream
yield
sys.stdout = sys.__stdout__
output = io.StringIO
with redirect_stdout(output):
...
【讨论】:
【参考方案6】:打印到终端会很慢。不幸的是,没有编写一个新的终端实现,我真的看不出你会如何显着加快速度。
【讨论】:
以上是关于为啥打印到标准输出这么慢?可以提速吗?的主要内容,如果未能解决你的问题,请参考以下文章
为啥在 ERR 陷阱中退出 0 时 bash 会抑制标准输出?