为什么打印到标准输出这么慢? 能不能加快速度?

我惊讶/沮丧地发现,简单地用打印语句输出到终端都需要很长时间。在最近打印一些慢日志记录后,我决定调查看看,并相当惊讶地发现几乎所有的时间花费在等待终端处理结果。

是否可以以某种方式加快写入标准输出的速度?

我写了一个脚本(在这个问题的底部'print_timer.py')来比较将100k行写入标准输出、文件和标准输出重定向到/dev/null时的时间。计时结果如下:

$ python print_timer.py
this is a test
this is a test

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终端放大到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秒内将+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
73272 次浏览

打印到终端会很慢。不幸的是,由于没有编写一个新的终端实现,我真的看不出你如何显著地加快这个速度。

您的重定向可能什么也不做,因为程序可以确定它们的输出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

我不能谈论技术细节,因为我不知道,但这并不让我感到惊讶:终端并不是为打印这样大量的数据而设计的。实际上,您甚至提供了一个链接到大量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):
...

除了输出可能默认为行缓冲模式外,输出到终端也会导致数据以最大吞吐量流入终端和串行线,或者伪终端和处理显示事件循环的单独进程,从某种字体呈现字符,移动显示位以实现滚动显示。后一种情况可能分布在多个进程上(例如,telnet服务器/客户端、终端应用程序、X11显示服务器),因此也存在上下文切换和延迟问题。

为什么写到物理磁盘比写到“屏幕”(大概是一个全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

我在我的机器上运行了你的文件编写测试,加上缓冲,10万行也只有0.05秒。

但是,使用上述修改为无缓冲写入,将1000行写入磁盘需要40秒。我放弃了等待10万行来写,但从前面的推断,它将需要超过一个小时

这样就能看清终端的11秒了,不是吗?

所以回答你最初的问题,写到一个终端实际上是非常快的,所有的事情都考虑到了,并且没有太多的空间让它更快(但是每个终端的工作量确实有所不同;请看Russ对这个答案的评论)。

(你可以添加更多的写缓冲,就像磁盘I/O一样,但是在缓冲区被刷新之前,你不会看到什么被写入到你的终端。这是一种权衡:交互性与批量效率。)

感谢所有的评论!在你的帮助下,我自己回答了这个问题。不过,回答自己的问题会让人感觉很不舒服。

问题1:为什么打印到标准输出很慢?

答:打印到标准输出天生很慢。是你使用的终端慢了。而且它与应用程序端的I/O缓冲几乎没有关系(例如:python文件缓冲)。见下文。

问题2:能不能加快速度?

答:是的,它可以,但似乎不是从程序端(进行“打印”到标准输出的端)。要加快速度,请使用更快的不同终端模拟器。

解释……

我尝试了一个自称为“轻量级”的终端程序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.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

记录的时间是手动收集的,但它们非常一致。我记录了最好的(大概)值。很明显,YMMV。

作为奖励,这是一个有趣的旅行,一些各种终端模拟器可用的!我很惊讶我的第一个“替代”测试结果是最好的。