我在Stack Overflow上看到了很多答案和评论提到做一些事情来躲避潜艇。在某些情况下案例中,给出了一个功能原因(大多数情况下,可能需要读取变量在内部分配的子shell之外),但在在其他情况下,回避似乎被视为目的本身。例如
-
tsv文件的两列的并集
建议{ ... ; } | ...
而不是( ... ) | ...
,所以无论哪种方式都有一个子shell。 -
使用sed和mv命令取消隐藏unix中的隐藏文件
-
Linux bash脚本复制文件
显式声明,"目标只是避免潜艇">
为什么会这样?它是为了时尚/优雅/美丽吗?对于性能(避免叉子)?用于防止可能虫子?还有别的吗?
发生了一些事情
首先,当分叉子shell只发生一次时,它可能不会引起注意,但如果你在循环中进行,它会对性能产生可衡量的影响。在Windows等平台上,分叉不像在现代Unixlikes上那样便宜,对性能的影响也更大。
其次,派生子shell意味着你有多个上下文,在它们之间切换时信息会丢失——如果你更改代码以在子shell中设置变量,那么当子shell退出时,该变量就会丢失。因此,代码中的子shell越多,以后修改代码时就越要小心,以确保所做的任何状态更改都能持续存在。
请参阅BashFAQ#24,了解一些由子shell引起的令人惊讶的行为示例。
有时示例会有所帮助。
f='fred';y=0;time for ((i=0;i<1000;i++));do if [[ -n "$( grep 're' <<< $f )" ]];then ((y++));fi;done;echo $y
real 0m3.878s
user 0m0.794s
sys 0m2.346s
1000
f='fred';y=0;time for ((i=0;i<1000;i++));do if [[ -z "${f/*re*/}" ]];then ((y++));fi;done;echo $y
real 0m0.041s
user 0m0.027s
sys 0m0.001s
1000
f='fred';y=0;time for ((i=0;i<1000;i++));do if grep -q 're' <<< $f ;then ((y++));fi;done >/dev/null;echo $y
real 0m2.709s
user 0m0.661s
sys 0m1.731s
1000
正如您所看到的,在这种情况下,在子shell中使用grep和参数扩展来进行相同的基本测试之间的差异在总时间上接近100倍。
根据问题的进一步发展,并考虑到下面的评论,这些评论显然没有表明他们试图表明的内容,我检查了以下代码:https://unix.stackexchange.com/questions/284268/what-is-the-overhead-of-using-subshells
time for((i=0;i<10000;i++)); do echo "$(echo hello)"; done >/dev/null
real 0m12.375s
user 0m1.048s
sys 0m2.822s
time for((i=0;i<10000;i++)); do echo hello; done >/dev/null
real 0m0.174s
user 0m0.165s
sys 0m0.004s
事实上,这比我预期的要糟糕得多。事实上,总时间慢了几乎两个数量级,系统调用时间慢了近三个数量级。这绝对令人难以置信。https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html
请注意,演示这一点的目的是表明,如果你使用的测试方法很容易养成使用subshell grep、sed或gawk(或bash内置,如echo)的习惯,这对我来说是一个坏习惯,我在快速黑客攻击时往往会养成这个习惯,那么值得意识到的是,这将对性能产生重大影响,如果bash内置程序能够以本机方式处理任务,那么避免这些操作可能是值得的。
通过仔细审查大型程序使用子shell的情况,并在可能的情况下用其他方法替换它们,我能够在刚刚完成的一组优化中减少大约10%的总体执行时间(不是第一次,也不是最后一次,我这样做的时候,它已经优化了好几次,所以再获得10%实际上是非常重要的)
所以值得注意。
因为我很好奇,我想确认"时间"在这里告诉我们什么:https://en.wikipedia.org/wiki/Time_(Unix)
总CPU时间是CPU或CPU为程序执行某些操作所花费的时间他们花在对程序的内核执行系统调用上利益当程序在数组中循环时,它正在累积用户CPU时间。相反,当程序执行诸如exec或fork,它正在累积系统CPU时间。
正如您在echo循环测试中看到的那样,就系统对内核的调用而言,fork的成本非常高,这些fork实际上加起来了(在sys调用上花费了700x!!!更多的时间)。
我正在解决其中一些问题,所以这些问题实际上与我和喜欢该程序的全球用户群体非常相关,也就是说,这对我来说不是一个神秘的学术观点,这是现实世界,具有真正的影响。
好吧,下面是我对为什么这很重要的解释:这是答案#2!
即使是在避免一个子shell的情况下,性能也会有很大的提高……叫我Obvious先生吧,但这种想法背后的概念与避免无用地使用<insert tool here>
(如cat|grep
、sort|uniq
甚至cat|sort|uniq
等)背后的概念是一样的。
这个概念就是Unix哲学,ESR通过引用KISS:保持简单,愚蠢
我的意思是,如果你写一个脚本,你永远不知道它最终会如何使用,所以你能腾出的每一个小字节或周期都很重要,所以如果你的脚本最终消耗了数十亿行的输入,那么它将被优化那么多叉/字节/。
我认为,除非另有要求,否则避免创建额外的shell进程是有意义的。
然而,在太多的情况下,任何一种都可以使用,其中一种比另一种更有意义,可以说一种方式总体上比另一个更好。在我看来,这纯粹是一种情境。