关于python:为什么打印到stdout这么慢?

Why is printing to stdout so slow? Can it be sped up?

我一直对用打印语句简单地输出到终端需要多长时间感到惊讶/沮丧。在最近一些令人痛苦的缓慢日志记录之后,我决定研究它,并且非常惊讶地发现几乎所有花费的时间都在等待终端处理结果。

写到stdout的速度可以加快吗?

我写了一个脚本("print_timer.py"在这个问题的底部)来比较将100k行写入stdout、文件以及将stdout重定向到/dev/null时的时间。以下是计时结果:

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的write()调用都会在实际写入物理磁盘之前返回。(操作系统稍后会这样做,将数千个单独的写入操作组合成一个大的、有效的块。)

另一方面,终端很少或没有缓冲:每个单独的print/write(line)等待完全写入(即显示到输出设备)完成。

为了使比较公平,您必须使文件测试使用与终端相同的输出缓冲,您可以通过将示例修改为:

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的端)。要加快速度,请使用速度更快的不同终端仿真器。

说明…

我尝试了一个自我描述的"轻量级"终端程序,名为wterm,结果明显更好。下面是我的测试脚本的输出(在问题的底部),当我在同一个系统中以1920x1200的速度在wterm中运行时,基本打印选项使用gnome终端花费了12秒:

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秒好得多!我不知道wterm是否更明智地按照我所建议的方式(以合理的帧速率渲染"可见"尾),或者它是否只是比gnome-terminal"少"。不过,就我的问题而言,我得到了答案。gnome-terminal慢。

所以-如果你有一个长时间运行的脚本,你觉得它很慢,它会向stdout发送大量的文本…尝试不同的终端,看看是否更好!

注意,我几乎随机地从Ubuntu/Debian存储库中提取了wterm。这个链接可能是同一个终端,但我不确定。我没有测试任何其他终端模拟器。

更新:因为我不得不抓狂,所以我用相同的脚本和全屏(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的stdout流行为相同)。

作为一个有趣的实验,尝试通过管道将输出传输到cat

我试过自己有趣的实验,结果如下。

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操作!注意,如果使用pythonw调用脚本,则不需要15秒;这完全是一个GUI问题。将stdout重定向到一个文件以避免:

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显示服务器),因此也存在上下文切换和延迟问题。