问题:为什么打印到标准输出这么慢?可以加快速度吗?
我一直对使用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转储到垃圾中一样快?
此链接讨论了终端如何阻止I / O,以便它可以“解析[输入],更新其帧缓冲区,与X服务器通信以滚动窗口等等” ……但是我不知道完全了解它。可能要花这么长时间?
我期望没有出路(缺少更快的tty实现?),但是无论如何我都会问。
更新:阅读了一些评论后,我想知道屏幕尺寸实际上对打印时间有多大影响,这确实有一定意义。上面最慢的数字是我的Gnome终端被炸毁为1920×1200。如果我减小很小,我得到…
-----
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倍),但不会改变我的问题。这只会增加我的问题,因为我不明白为什么终端屏幕渲染会减慢应用程序向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
写入物理磁盘比写入“屏幕”(大概是全RAM操作)要快得多,并且实际上与使用/ dev / null转储到垃圾中一样快?
恭喜,您刚刚发现了I / O缓冲的重要性。:-)
磁盘似乎速度更快,因为它具有很高的缓冲能力:write()
在将任何内容实际写入物理磁盘之前,所有Python的调用都将返回。(操作系统稍后执行此操作,将成千上万的单个写入合并为一个大而有效的块。)
另一方面,终端几乎不执行缓冲或不进行缓冲:每个人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
我在我的机器上运行了文件写入测试,并通过缓冲在100,000行中也进行了0.05s的测试。
但是,通过上述修改以无缓冲方式写入数据,只需要40秒就可以将1,000行写入磁盘。我放弃了等待100,000行的写操作,但是从以前的内容推论得出,这将花费一个多小时。
这使航站楼的11秒成为现实,不是吗?
因此,要回答您最初的问题,考虑到所有因素,实际上写信到终端的速度非常快,并且没有太多的空间可以使它更快(但是各个终端的工作量有所不同;请参阅Russ对此的评论)回答)。
(您可以像使用磁盘I / O一样添加更多的写缓冲,但是直到刷新缓冲区之后,您才能看到向终端写入的内容。这是一个折衷方案:交互性与大容量效率。)
回答 1
感谢所有的评论!我最终在您的帮助下自行回答。不过,回答您自己的问题感觉很脏。
问题1:为什么打印到标准输出速度慢?
答:打印到标准输出并不是天生就慢。您正在使用的终端很慢。它与应用程序端的I / O缓冲(例如python文件缓冲)几乎为零。见下文。
问题2:可以加快速度吗?
答:是的,可以,但是似乎不是从程序方面(将“打印”到stdout的那一侧)进行。为了加快速度,请使用更快的其他终端仿真器。
说明…
我尝试了一个自描述为“轻量级”的终端程序,wterm
并获得了明显更好的结果。下面是在wterm
同一系统上以1920×1200 运行时,我的测试脚本的输出(位于问题的底部),该系统使用gnome-terminal的基本打印选项花费了12s:
----- 时序摘要(每条10万行) ----- 打印:0.261 s 写入文件(+ fsync):0.110 s 用stdout = / dev / null打印:0.050 s
0.26s比12s好得多!我不知道是否wterm
更聪明地按照我的建议进行渲染(以合理的帧频渲染“可见”尾巴),或者是否“做得比”少gnome-terminal
。为了我的问题,我得到了答案。 gnome-terminal
是慢的。
所以-如果您运行的脚本长时间运行,感觉很慢,并且会向stdout喷出大量文本,请尝试其他终端,看看它是否更好!
请注意,我几乎是wterm
从ubuntu / debian存储库中随机提取的。 该链接可能是同一终端,但是我不确定。我没有测试任何其他终端模拟器。
更新:因为必须要抓痒,所以我用相同的脚本和全屏(1920×1200)测试了一堆其他终端模拟器。我的手动收集的统计信息在这里:
wterm 0.3秒 间隔0.3秒 接收0.3秒 mrxvt 0.4s konsole 0.6秒 药师0.7s 接线柱7s xterm 9s gnome终端12s xfce4端子12s 巴拉终端18s xvt 48s
记录的时间是手动收集的,但是它们是相当一致的。我记录了最好的(ish)值。YMMV,显然。
另外,它是对其中可用的各种终端仿真器的一次有趣的浏览!我很惊讶我的第一个“替代”测试竟然是同类中最好的。
回答 2
重定向可能什么也不做,因为程序可以确定其输出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
我无法谈论技术细节,因为我不知道这些细节,但这并不令我感到惊讶:该终端并非为打印此类数据而设计的。的确,您甚至提供了指向您每次打印某些内容时必须要做的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
打印到终端将很慢。不幸的是,如果没有编写新的终端实现,我真的看不到您如何显着加快这一步。
回答 5
除了输出可能默认为行缓冲模式外,输出到终端还导致您的数据以最大的吞吐量流入终端和串行线,或者是伪终端和单独的处理显示的进程事件循环,从某种字体渲染字符,移动显示位以实现滚动显示。后一种情况可能分布在多个进程(例如telnet服务器/客户端,终端应用程序,X11显示服务器)上,因此也存在上下文切换和延迟问题。