Why is printing to stdout so slow? Can it be sped up?
我一直对用打印语句简单地输出到终端需要多长时间感到惊讶/沮丧。在最近一些令人痛苦的缓慢日志记录之后,我决定研究它,并且非常惊讶地发现几乎所有花费的时间都在等待终端处理结果。
写到stdout的速度可以加快吗?
我写了一个脚本("
1 2 3 4 5 6 7 8 9 10 11 | $ 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或其他事情,我在脚本外部进行了重定向…
1 2 3 4 5 6 7 | $ 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的速度有多慢。怎么可能写入物理磁盘的速度比写入"screen"(大概是一个全RAM操作)快得多,并且有效地像简单地使用/dev/null转储到垃圾一样快?
这个链接讨论了终端如何阻止I/O,以便它可以"解析[输入]、更新其帧缓冲区、与X服务器通信以滚动窗口等等"…但我不完全明白。怎么会这么久?
我希望没有出路(缺少更快的TTY实现?)但我还是会问。
更新:在阅读了一些评论之后,我想知道我的屏幕尺寸对打印时间有多大的影响,它确实有一些意义。上面的数字很慢,我的GNOME终端爆炸到1920x1200。如果我把它减得很小,我会…
1 2 3 4 5 6 | ----- 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程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 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 " % (cmd, t) #Add a newline to match line outputs above... line +=" " 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 " % (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 " % (cmd, t) print >> sys.stderr,"-----" print >> sys.stderr,"timing summary (100k lines each)" print >> sys.stderr,"-----" print >> sys.stderr, summary |
型
How can it be that writing to physical disk is WAY faster than writing to the"screen" (presumably an all-RAM op), and is effectively as fast as simply dumping to the garbage with /dev/null?
号
恭喜您,您刚刚发现了I/O缓冲的重要性。:-)
磁盘似乎更快,因为它是高度缓冲的:所有python的
另一方面,终端很少或没有缓冲:每个单独的
为了使比较公平,您必须使文件测试使用与终端相同的输出缓冲,您可以通过将示例修改为:
1 2 3 4 5 | 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秒。
但是,通过以上对写不缓冲的修改,只需要40秒就可以将1000行写入磁盘。我放弃了等待100000行的写作时间,但从前面的推测来看,要花一个多小时。
这让终端的11秒进入了视野,不是吗?
因此,要回答你最初的问题,给终端写信实际上是非常快的,所有的事情都考虑到了,而且没有太多的空间让它更快(但是各个终端在工作多少方面确实有所不同;请参阅Russ对这个答案的评论)。
(您可以添加更多的写缓冲,比如磁盘I/O,但是在缓冲被刷新之前,您将看不到写入终端的内容。这是一种权衡:互动与批量效率。)
谢谢你的评论!最后我在你的帮助下自己回答了。不过,回答自己的问题会让人感觉很不舒服。
问题1:为什么打印到stdout速度慢?
答:打印到stdout本身并不慢。这是你工作的终点站,速度很慢。它几乎与应用程序端的I/O缓冲无关(例如:python文件缓冲)。见下文。
问题2:能加快速度吗?
答:是的,它可以,但似乎不是从程序端(执行"打印"到stdout的端)。要加快速度,请使用速度更快的不同终端仿真器。
说明…
我尝试了一个自我描述的"轻量级"终端程序,名为
1 2 3 4 5 6 | ----- 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秒好得多!我不知道
所以-如果你有一个长时间运行的脚本,你觉得它很慢,它会向stdout发送大量的文本…尝试不同的终端,看看是否更好!
注意,我几乎随机地从Ubuntu/Debian存储库中提取了
更新:因为我不得不抓狂,所以我用相同的脚本和全屏(1920x1200)测试了一整堆其他终端模拟器。我手动收集的统计信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 | 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)值。很明显。
作为一个额外的好处,它是一个有趣的旅行,一些不同的终端仿真器提供了那里!我很惊讶我的第一个"备用"测试竟然是最好的。
型
由于程序可以确定其输出fd是否指向tty,因此重定向可能不起作用。
当指向终端时,stdout很可能是行缓冲的(与c的
作为一个有趣的实验,尝试通过管道将输出传输到
我试过自己有趣的实验,结果如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $ 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操作!注意,如果使用
1 2 3 4 5 6 7 8 9 10 11 | 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显示服务器),因此也存在上下文切换和延迟问题。