我有这个简单的 bash 脚本:
#!/bin/bash
for i in {1..1000000}
do
echo "hello ${i}"
done
这会打印一条消息一百万次。
tee
我尝试比较将所有输出转储到单个文件中与使用将输出拆分为两个文件的性能。
$ time ./run.sh > out1.txt
real 0m9.535s
user 0m6.678s
sys 0m2.803s
$
$ time ./run.sh | tee out2.txt > out1.txt
real 0m6.705s
user 0m6.895s
sys 0m5.214s
事实证明,同时写入两个文件比仅写入一个文件要快(这些结果在多次运行中是一致的)。
谁能解释这怎么可能?另外,我应该如何解释user
和sys
的输出time
?为什么sys
使用时时间会变长tee
?
答案1
首先,让我们澄清几件事。
- 法拉盛
shell 在每个内置命令之后刷新其输出,例如echo
在本例中。
必须如此。它必须模仿行为,就好像它是外部命令一样(只要有可能,就像这里一样,它是一个内置的 shell 命令,而不是/bin/echo
纯粹出于性能原因的外部命令)。外部命令显然必须在退出时刷新(或忽略)输出。内置函数echo
必须具有相同的行为方式。
这是预期的行为,如果echo
脚本中的 an 之后是长时间等待或计算,则echo
-ed 数据已在输出中可用。
此行为与标准输出的连接位置无关。它可能是终端、管道、文件,都没关系。每个echo
都是独立冲洗的。 (不要与默认libc 的行为,其中行为取决于标准输出是否发送到终端,如您所见最多标准实用程序的,例如cat
、grep
、head
等等,当然还有tee
。 shell 在每个内置命令之后都会显式刷新,而不是依赖 libc 的默认缓冲。)
用于strace ./run.sh > out1.txt
查看write()
shell 执行的一百万次调用。
- 多核
我假设您的系统中有多个 CPU 核心,并且没有其他进程造成的显着负载。在此设置中,内核分配bash run.sh
给一个核心,然后分配tee
给另一个核心。这样就不会发生重量级的进程切换,并且如果它们实际上都可以运行,那么它们确实会同时运行。
据推测,如果您将两个进程限制在一个核心中(我相信您可以使用命令来执行此操作taskset
,我会让您尝试一下),那么您会得到截然不同的结果,tee
从而显着减慢进程。它不仅仅是一个需要串行运行、交错运行的额外进程run.sh
,而且内核还需要在两个进程之间多次切换,而这种切换本身的成本也相当高。
- 命令
time
time
测量整个管道,即run.sh
组合tee
管道。如果您只想测量其中一个命令,请time
在子 shell 中调用,例如:
$ ( time ./run.sh ) | tee out2.txt > out1.txt
$ ./run.sh | ( time tee out2.txt ) > out1.txt
对于real
时间,打印挂钟经过的时间。也就是说,就好像您从字面上打印了管道之前和之后的时间戳并计算了差异,或者使用了外部秒表。如果两个进程在管道中运行,每个进程旋转一个 CPU 核心 10 秒,而它们都可以一直运行,完全并行,则实际时间将为 10 秒。
user
sys
然而,时间是跨 CPU 核心累加的。如果两个并行进程,每个进程都在自己的 CPU 核心上,将 CPU 旋转到最大值 10 秒(正如我们刚刚看到的,实际时间为 10 秒),则用户时间将为 20 秒。
现在,让我们把这些放在一起:
只剩下一个问题需要回答:为什么将一小块数据写入管道比写入文件更快?
我对此没有直接的答案,我只是向后得出结论,即根据您测量的计时结果,它必须是写入管道比写入文件更快。以下是一些猜测,但希望是合理的。
管道有固定的大小(我认为是 64kB)。很容易在创建管道时分配整个大小,因此内核中不再发生动态分配。 (如果达到大小,写入端会阻塞,直到读取器释放一些空间。)但是,对于文件来说,没有这样的限制。无论从用户空间传递到内核的任何内容都必须复制到那里(在数据实际写入磁盘之前阻止写入器是不可行的)。因此我发现很可能是某种动态内存分配可能写入文件时发生,使得这部分的成本更高。
对于管道,内核可能需要做的唯一额外的事情是唤醒刚刚能够运行的进程,即等待数据出现在管道中。对于文件,内核需要更新文件的内存中元数据(大小、修改时间),并启动计时器(或更新现有计时器)以安排最终将此数据写入磁盘。
没有严格的规则规定写入文件会不得不比写入管道更昂贵,显然它确实如此,正如您测量的数字所证明的那样。
通过添加tee
,您恰好减少了 所需的工作run.sh
,因为它的百万write()
秒现在恰好便宜一些。这使得整个系统run.sh
能够运行得更快,从而导致更短的挂钟时间。
您添加了第二个进程,该进程主要与其并行运行,并且可能完成较少的工作。它的两个输出文件都使用缓冲write()
,即与无缓冲的情况相比,只有几个系统调用。对于它的输入,它可能会执行一百万个tiny read()
s,但我会猜测由于时间上的随机性以及其他原因,许多bash
swrite()
可能会被组合并以单个 s 的形式到达read()
,因此可能需要明显少于一百万read()
s。 (看到read()
它执行了多少次真是太酷了。strace
“ing”不是一个选项,因为测量本身会显着改变计时。我会通过修补tee
来增加每个计数器的计数器read()
,并在最后转储该数字.我会把它留给亲爱的读者作为练习。)
因此,tee
比 更快run.sh
,因此不会延迟管道的完成。但是,它向用户和系统时间添加了自己的份额,使它们比以前更大。
更新:
我很好奇,所以我修补了一下,tee
看看有多少次read()
。
如果桌面上只有一个终端,则约为 660 000 - 670 000。如果在后台打开浏览器并显示一两页,则约为 500 000 - 600 000。浏览器刚刚启动(工作量更大),大约是 400 000。这是有道理的:要做的其他事情越多,就越有可能tee
不会立即读取其数据,并且某些bash
' write()
' 会累积。您明白了这个想法,现在也有了粗略的数字,当然,不同计算机的粗略数字可能会有很大差异。