为什么打印到标准输出这么慢?可以加快速度吗?


166

我一直对使用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终端被炸毁为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倍),但不会改变我的问题。这只会增加我的问题,因为我不明白为什么终端屏幕渲染会减慢应用程序向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

9
写入标准输出的全部目的是使人们可以读取输出。世界上没有人可以在12秒内阅读10,000行文本,那么使stdout更快的意义何在?
Seen Osewa

14
@Seun Osewa:一个例子(引发我的问题)是在执行诸如print语句调试之类的事情时。您想运行程序并查看结果。您显然是正确的,大多数行都将通过您看不见的行进路线,但是当发生异常时(或者您小心地放置了有条件的getch / raw_input / sleep语句),您想要直接查看打印输出而不是经常需要打开或刷新文件视图。
罗斯,2010年

3
打印语句调试是tty设备(即终端)默认使用行缓冲而不是块缓冲的原因之一:如果程序挂起且调试输出的最后几行仍在缓冲区中,则调试输出的用处不大而不是冲洗到终端。
Stephen C. Steel

@Stephen:这就是为什么我没有花太多精力去追求一位评论者通过提高缓冲区大小来声称的巨大改进。它完全无法达到调试打印的目的!在调查期间,我确实做了一些实验,但没有发现任何净改进。我仍然对差异感到好奇,但并非如此。
罗斯,2010年

有时,对于运行时间非常长的程序,我会每n秒打印一次当前的行标准输出-类似于curses应用程序中的刷新延迟。这不是完美的,但可以让我对我偶尔会遇到的情况有所了解。
rkulla'1

Answers:


155

写入物理磁盘比写入“屏幕”(大概是全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一样添加更多的写缓冲,但是直到刷新缓冲区之后,您才能看到向终端写入的内容。这是一个折衷方案:交互性与大容量效率。)


6
我得到了I / O缓冲...您肯定提醒我,我应该进行fsync来真正比较完成时间(我将更新问题),但是每行执行fsync 是精神错乱。tty是否真的需要有效地做到这一点?是否没有相当于文件的终端/操作系统侧缓冲?即:应用程序写入标准输出,并在终端渲染到屏幕之前返回,终端(或操作系统)将其全部缓冲。然后,终端可以以可见的帧速率明智地将尾巴渲染到屏幕上。有效阻止每一行似乎很愚蠢。我觉得我仍然缺少一些东西。
拉斯

您可以使用来打开带有大缓冲区的stdout句柄os.fdopen(sys.stdout.fileno(), 'w', BIGNUM)。但是,这几乎永远不会有用:几乎所有应用程序都必须记住在每行用户输入的输出之后显式刷新。
Pi Delport

1
我之前曾尝试使用巨大的fp = os.fdopen(sys.__stdout__.fileno(), 'w', 10000000)python缓冲区(最大10 MB )。影响为零。即:tty延迟仍然很长。这让我认为/意识到您只是推迟了缓慢的tty问题...当python的缓冲区最终刷新tty时,在返回之前,似乎仍然对流执行相同的总处理量。
罗斯,2010年

8
请注意,此答案具有误导性和错误性(抱歉!)。具体地说,说“没有足够的空间使它更快(超过11秒)”是错误的。请查看我对这个问题的回答,我将显示wterm终端在0.26s内获得相同的11s结果。
罗斯,2010年

2
拉斯:感谢您的反馈!在我这一边,更大的fdopen缓冲区(2MB)无疑带来了巨大的不同:打印时间从几秒钟减少到0.05s,与文件输出相同(使用gnome-terminal)。
Pi Delport

88

感谢所有的评论!我最终在您的帮助下自行回答。不过,回答您自己的问题感觉很脏。

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

答:打印到标准输出并不是天生就慢。您正在使用的终端很慢。它与应用程序端的I / O缓冲(例如python文件缓冲)几乎为零。见下文。

问题2:可以加快速度吗?

答:是的,可以,但是似乎不是从程序方面(将“打印”到stdout的那一侧)进行。为了加快速度,请使用更快的其他终端仿真器。

说明...

我尝试了一个自描述为“轻量级”的终端程序,wterm并获得了明显更好的结果。下面是在wterm同一系统上以1920x1200 运行时,我的测试脚本的输出(位于问题的底部),该系统使用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存储库中随机提取的。 该链接可能是同一终端,但是我不确定。我没有测试任何其他终端模拟器。


更新:因为必须要抓痒,所以我用相同的脚本和全屏(1920x1200)测试了一堆其他终端模拟器。我的手动收集的统计信息在这里:

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,显然。

另外,它是对其中可用的各种终端仿真器的一次有趣的浏览!我很惊讶我的第一个“替代”测试竟然是同类中最好的。


1
您也可以尝试一个学期。这是我使用您的脚本进行测试的结果。Aterm-打印:0.491 s,写入文件(+ fsync):0.110 s,使用stdout打印= / dev / null:0.087 s wterm-打印:0.521 s,写入文件(+ fsync):0.105 s,使用stdout打印=
/dev/null:0.085

1
urxvt与rxvt相比如何?
2010年

3
另外,,screen(程序)应包括在列表中!(或byobu,它是screen增强功能的包装)。此实用程序允许具有多个终端,非常类似于X终端中的选项卡。我假设打印到当前screen的终端与打印到普通终端是相同的,但是在screen终端的一个终端上打印然后又不进行任何活动又切换到另一个终端怎么办?
ArmandoPérezMarqués2010年

1
很奇怪,前一段时间我在速度方面比较了不同的终端,而gnome终端在相当严格的测试中表现最好,而xterm则最慢。从那时起,也许他们一直在努力进行缓冲。另外,对unicode的支持也可以起到很大的作用。
Tomas Pruzina 2012年

2
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
rkulla,2015年

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

我没想到python检查其输出FS。我想知道python是否在幕后诱骗?我希望不会,但不知道。
罗斯,2010年

+1指出缓冲中最重要的差异
Peter G.

@Russ:该-u选项强制stdinstdout并且要不stderr进行缓冲,这比被块缓冲要慢(由于开销)
Hasturkun 2010年

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):
    ...

3

打印到终端将很慢。不幸的是,如果没有编写新的终端实现,我真的看不到您如何显着加快这一步。


2

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


真正!这促使我尝试将终端窗口大小(在Gnome中)减小到微不足道(从1920x1200开始)。果然... 2.8s的打印时间与11.5s的打印时间。好多了,但仍然...为什么停顿了?您可能会认为stdout缓冲区(hmm)可以处理所有100k行,并且终端显示将仅从缓冲区的尾部抓取屏幕上可以容纳的任何内容,并快速完成。
罗斯,2010年

如果xterm(或gterm,在这种情况下)认为自己也不必同时显示所有其他输出,则可以更快地渲染最终屏幕。如果试图走这条路,则可能会使小屏幕更新的常见情况显得反应迟钝。在编写此类软件时,有时您可以通过使用不同的模式来处理它,并尝试检测何时需要从小型操作模式变为批量操作模式。您可以cat big_file | tail甚至cat big_file | tee big_file.cpy | tail经常使用它来加快速度。
nategoose 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.