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