为什么打印到标准输出的速度这么慢?可以加快速度吗?
- 2025-03-05 09:14:00
- admin 原创
- 109
问题描述:
我一直对使用 print 语句简单地输出到终端所花费的时间感到惊讶/沮丧。在最近经历了一些非常缓慢的日志记录后,我决定研究一下,并惊讶地发现几乎所有的时间都花在等待终端处理结果上。
可以以某种方式加快写入标准输出的速度吗?
我编写了一个脚本(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
" % (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
解决方案 1:
为什么写入物理磁盘比写入“屏幕”(大概是全 RAM 操作)要快得多,并且实际上与使用 /dev/null 简单地转储到垃圾一样快?
恭喜,您刚刚发现了 I/O 缓冲的重要性。:-)
磁盘似乎更快,因为它具有高度缓冲:所有 Pythonwrite()
调用都会在实际将任何内容写入物理磁盘之前返回。(操作系统稍后会执行此操作,将数千个单独的写入组合成一个大而高效的块。)
另一方面,终端很少或根本不进行缓冲:每个单独的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.05 秒。
但是,通过上述修改,以非缓冲方式写入,需要 40 秒才能将 1,000 行写入磁盘。我放弃了等待 100,000 行写入的想法,但根据之前的情况推断,这将需要一个多小时。
这样就让我们对航站楼的 11 秒有了更直观的认识,不是吗?
因此,要回答你最初的问题,从各方面来看,写入终端的速度实际上非常快,并且没有太多的空间可以使其更快(但各个终端在执行的工作量上确实有所不同;请参阅 Russ 对这个答案的评论)。
(您可以添加更多的写入缓冲,例如磁盘 I/O,但之后您才会看到写入终端的内容,直到缓冲区被刷新之后。这是一个权衡:交互性与批量效率。)
解决方案 2:
感谢大家的评论!在你们的帮助下,我最终自己回答了这个问题。不过,自己回答问题感觉很不礼貌。
问题 1:为什么打印到标准输出很慢?
答案:打印到 stdout本身并不慢。慢的是你使用的终端。而且它与应用程序端的 I/O 缓冲(例如:python 文件缓冲)几乎没有关系。见下文。
问题2:可以加快速度吗?
答案:是的,可以,但从程序端(向 stdout 进行“打印”的端)看来不行。要加快速度,请使用更快的其他终端仿真器。
解释...
我尝试了一个自称为“轻量级”的终端程序wterm
,并获得了明显更好的结果。下面是我的测试脚本(在问题底部)在 1920x1200 分辨率下运行时的输出,wterm
在同一系统上使用 gnome-terminal 的基本打印选项需要 12 秒:
-----
时间摘要(每篇 10 万行)
-----
打印:0.261 秒
写入文件 (+fsync) :0.110 秒
使用 stdout = /dev/null 打印:0.050 秒
0.26 秒比 12 秒好多了!我不知道 是否wterm
更智能地按照我建议的方式渲染到屏幕上(以合理的帧速率渲染“可见”的尾部),或者它只是“做得更少” gnome-terminal
。不过,就我的问题而言,我已经找到了答案。 gnome-terminal
很慢。
所以 - 如果您有一个长时间运行的脚本,您觉得它很慢并且它会向标准输出大量文本......请尝试使用不同的终端,看看是否有所改善!
请注意,我几乎是随机wterm
从 ubuntu/debian 存储库中提取的。 此链接可能是同一个终端,但我不确定。我没有测试任何其他终端仿真器。
更新:因为我不得不解决这个问题,所以我用相同的脚本和全屏(1920x1200)测试了一大堆其他终端仿真器。我手动收集的统计数据如下:
冬季 0.3秒
0.3秒
接收 0.3秒
mrxvt 0.4秒
控制台 0.6秒
yakuake 0.7s
外部终端 7s
xterm 9s
gnome 终端 12s
xfce4-终端 12s
vala-18s 号航站楼
48秒
记录的时间是手动收集的,但它们相当一致。我记录了最佳值。显然,YMMV 不同。
作为奖励,这是一次有趣的旅程,带我了解了市面上各种终端模拟器!我很惊讶我的第一个“替代”测试竟然是最好的。
解决方案 3:
您的重定向可能没有任何作用,因为程序可以确定它们的输出 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
解决方案 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):
...
解决方案 5:
打印到终端会很慢。遗憾的是,除非编写新的终端实现,否则我真的看不出如何显著加快速度。
解决方案 6:
除了输出可能默认为行缓冲模式之外,输出到终端还会导致数据流入具有最大吞吐量的终端和串行线路,或流入伪终端和单独的进程,该进程处理显示事件循环、渲染某种字体的字符、移动显示位以实现滚动显示。后一种情况可能分布在多个进程中(例如 telnet 服务器/客户端、终端应用程序、X11 显示服务器),因此也存在上下文切换和延迟问题。
扫码咨询,免费领取项目管理大礼包!