我一直对使用print语句输出到终端需要多长时间感到惊讶/沮丧。在最近的一些令人痛苦的缓慢记录之后,我决定调查它并且很惊讶地发现几乎所有花费的时间等待终端处理结果。
能不能以某种方式加快对stdout的写作?
我在这个问题的底部写了一个脚本('print_timer.py
')来比较写入100k行到stdout,文件和stdout重定向到/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实施?)但无论如何我想问。
更新:在阅读了一些评论后,我想知道我的屏幕尺寸对打印时间的影响有多大,而且确实有一些意义。上面真正缓慢的数字是我的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),但不会改变我的问题。它只是添加到我的问题,因为我不明白为什么终端屏幕渲染应该减慢写入stdout的应用程序。为什么我的程序需要等待屏幕渲染继续?
所有终端/ tty应用程序是否都不相同?我还没有实验。在我看来,终端应该能够缓冲所有传入的数据,无形地解析/渲染它,并且只能以合理的帧速率渲染当前屏幕配置中可见的最新块。因此,如果我可以在~0.1秒内将+ fsync写入磁盘,终端应该能够以某种顺序完成相同的操作(可能会有一些屏幕更新)。
我仍然希望有一个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
答案 0 :(得分:144)
如何写入物理磁盘比写入“屏幕”(可能是一个全RAM操作)更快,并且实际上就像使用/ dev / null简单地转储到垃圾一样快?
恭喜,您刚刚发现了I / O缓冲的重要性。 : - )
磁盘出现更快,因为它是高度缓冲的:所有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
我在我的机器上运行了你的文件写入测试,并且通过缓冲,这里还有0.05秒,为100,000行。
但是,通过上面对无缓冲写入的修改,只需要40秒即可将1000行写入磁盘。我放弃了等待10万行写入,但从前一次推断,需要超过一小时。
这让终端的11秒进入视角,不是吗?
所以为了回答你原来的问题,写一个终端实际上速度非常快,考虑到所有事情,而且没有太大的空间让它快得多(但个别终端的工作量确实不同;见Russ的评论这个答案)。
(你可以添加更多写缓冲,就像磁盘I / O一样,但是在刷新缓冲区之前你不会看到写入终端的内容。这是一种权衡:交互性与批量效率。)
答案 1 :(得分:83)
感谢所有评论!我最后在你的帮助下自己回答了这个问题。但是,回答你自己的问题感觉很脏。
问题1:为什么打印到标准输出缓慢?
答案:打印到stdout 不本来就很慢。你工作的终端很慢。它与应用程序端的I / O缓冲几乎没有关系(例如:python文件缓冲)。见下文。
问题2:可以加速吗?
答案:是的它可以,但似乎不是来自程序方面(对'stdout'进行'打印'的一方)。要加快速度,请使用速度更快的终端仿真器。
...说明
我尝试了一个名为wterm
的自我描述的“轻量级”终端程序,并且显着更好的结果。下面是我的测试脚本的输出(在问题的底部),当在1920x1200的wterm
中运行时,在使用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.26s比12s好多了!我不知道wterm
是否更加智能地了解它如何根据我的建议(以合理的帧速率渲染“可见”尾部)进行筛选,或者它是否只是“做得更少”比gnome-terminal
。出于我的问题的目的,我得到了答案。 gnome-terminal
很慢。
所以 - 如果你有一个长时间运行的脚本,你觉得它很慢,它会向stdout喷出大量的文本...尝试一个不同的终端,看看它是否更好!
请注意,我几乎随机从ubuntu / debian存储库中提取wterm
。 This link可能是同一个终端,但我不确定。我没有测试任何其他终端模拟器。
更新:因为我不得不抓痒,我用相同的脚本和全屏(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
手动收集记录的时间,但它们非常一致。我记录了最佳(ish)值。 YMMV,显然。
作为奖励,这里有一些有趣的各种终端模拟器之旅!我很惊讶我的第一个“替代”测试结果是最好的。
答案 2 :(得分:13)
您的重定向可能无效,因为程序可以确定其输出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
答案 3 :(得分:4)
我不能谈论技术细节,因为我不知道它们,但这并不让我感到惊讶:终端不是设计用于打印这样的大量数据。实际上,您甚至可以提供一个链接,指向每次要打印时必须执行的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):
...
答案 4 :(得分:3)
打印到终端的速度会变慢。不幸的是,如果没有编写新的终端实现,我无法真正看到你如何显着提高速度。
答案 5 :(得分:2)
除了输出可能默认为线路缓冲模式之外,输出到终端还会导致数据流入具有最大吞吐量的终端和串行线路,或者伪终端和单独的进程,即处理显示事件循环,从某些字体渲染字符,移动显示位以实现滚动显示。后一种情况可能分布在多个进程(例如telnet服务器/客户端,终端应用程序,X11显示服务器)上,因此也存在上下文切换和延迟问题。