破折号或其他外壳比bash更快吗?


56

我一直认为使用破折号代替bash的唯一好处是破折号较小,因此许多破折号实例在启动时会更快地启动。

但是我进行了一些研究,发现有些人将所有脚本迁移到破折号以希望它们运行得更快,并且我也在Ubuntu Wiki的DashAsBinSh文章中找到了这一点:

切换默认Shell的主要原因是效率。bash是适合交互使用的出色的全功能外壳;实际上,它仍然是默认的登录外壳。但是,与破折号相比,它的启动和操作相当庞大且缓慢

如今,我在系统上的许多事情上都使用了很多bash脚本,但我的问题是我有一个特定的脚本,该脚本连续24/7运行,产生了大约200个孩子,这些孩子将我的计算机加热10° C比正常使用更多。

这是一个很大的脚本,带有很多bashism,因此将它们移植到POSIX或其他shell会非常耗时(并且POSIX对于个人使用而言并不重要),但是如果我可以减少其中的一部分,那将是值得的CPU使用率。我知道还需要考虑其他事项,例如调用外部二进制文件(如或代替的sed简单bashism)。${foo/bar}grep=~

与破折号相比,TL; DR的启动和操作真的比bash慢吗?还有其他比bash 更有效的 Unix shell 吗?


12
如果您打算移植它以提高性能,那么您认为完全使用其他某种语言(perl,python,ruby)会更好吗?我认为它们通常效率更高,尽管这将取决于任务的确切性质。
goldilocks

次要点:[也应该是内置的。
Mikel 2014年

2
请记住,与担心内存使用情况不同的是,差异主要显示的是您是在shell中而不是在外部程序中进行计算(即,您以错误的方式使用shell!);例如,在我的计算机上,使用while循环计数到一百万(不执行其他操作)的脚本在mksh / zsh中的速度快约2倍,而在破折号中的速度快2倍以上,但是在实际脚本中,我会尽可能多地卸载其他脚本程式。
loreb 2014年

3
bash过去很慢 最近它取得了很大的进步,但是对于大多数情况来说,它仍然比大多数其他Shell慢。
斯特凡Chazelas

1
不要使用简单的bashism[ "$foo" != "${foo#*bar}" ]处理您的grep内容。而sed事情:while [ "$foo" != "${foo#*bar}" ]; do s=$s${foo%%bar*} foo=${foo#*bar} ; done ; foo=$s$foo。您可以将任何东西放入函数中。
mikeserv

Answers:


38

外壳序列:

基准测试外壳性能的一种有用方法是反复进行许多非常小的,简单的评估。我认为,重要的是不仅要循环,而且要遍历输入,因为外壳需要读取<&0

我认为这将补充已经发布的测试@cuonglm,因为它演示了一次调用后单个shell进程的性能,而不是他的演示了调用时一个shell进程加载的速度。这样,我们之间,我们覆盖了硬币的两面。

这是一个方便演示的功能:

sh_bench() (                                               #dont copy+paste comments
    o=-c sh=$(command -v "$1") ; shift                     #get shell $PATH; toss $1
    [ -z "${sh##*busybox}" ] && o='ash -c'                 #cause its weird
    set -- "$sh" $o "'$(cat <&3)'" -- "$@"                 #$@ = invoke $shell
    time env - "$sh" $o "while echo; do echo; done|$*"     #time (env - sh|sh) AC/DC
) 3<<-\SCRIPT                                                                      
#Everything from here down is run by the different shells    
    i="${2:-1}" l="${1:-100}" d="${3:-                     
}"; set -- "\$((n=\$n\${n:++\$i}))\$d"                     #prep loop; prep eval
    set -- $1$1$1$1$1$1$1$1$1$1                            #yup
    while read m                                           #iterate on input
    do  [ $(($i*50+${n:=-$i})) -gt "$(($l-$i))" ] ||       #eval ok?
            eval echo -n \""$1$1$1$1$1"\"                  #yay!
        [ $((n=$i+$n)) -gt "$(($l-$i))" ] &&               #end game?
            echo "$n" && exit                              #and EXIT
        echo -n "$n$d"                                     #damn - maybe next time
    done                                                   #done 
#END
SCRIPT                                                     #end heredoc

它可以为每个换行读取一次增加一个变量,或者,如果可以的话,可以稍作优化,为每个换行读取增加50次。每次变量递增时,都会打印到stdout。它的行为很像seq十字nl

只是为了清楚地说明它的作用- set -x;这是在time上面的函数中插入之前的一些截断输出:

time env - /usr/bin/busybox ash -c '
     while echo; do echo; done |
     /usr/bin/busybox ash -c '"'$(
         cat <&3
     )'"' -- 20 5 busybox'

因此,每个壳首先被称为:

 env - $shell -c "while echo; do echo; done |..."

...以生成在读入时3<<\SCRIPTcat无论如何读入时都需要循环的输入。另一方面,|pipe它再次像这样调用自己:

"...| $shell -c '$(cat <<\SCRIPT)' -- $args"

因此,除了最初的调用之外env (因为cat实际上是在前一行中调用的);从被调用到退出之前,没有其他进程被调用。至少,我希望那是真的。

在数字之前...

我应该对可移植性做一些说明。

  • posh不喜欢$((n=n+1))并坚持$((n=$n+1))

  • mkshprintf大多数情况下没有内置函数。较早的测试使它滞后很多- /usr/bin/printf每次运行都会调用它。因此echo -n以上。

  • 也许我记得更多...

无论如何,数字:

for sh in dash busybox posh ksh mksh zsh bash
do  sh_bench $sh 20 5 $sh 2>/dev/null
    sh_bench $sh 500000 | wc -l
echo ; done

一劳永逸...

0dash5dash10dash15dash20

real    0m0.909s
user    0m0.897s
sys     0m0.070s
500001

0busybox5busybox10busybox15busybox20

real    0m1.809s
user    0m1.787s
sys     0m0.107s
500001

0posh5posh10posh15posh20

real    0m2.010s
user    0m2.060s
sys     0m0.067s
500001

0ksh5ksh10ksh15ksh20

real    0m2.019s
user    0m1.970s
sys     0m0.047s
500001

0mksh5mksh10mksh15mksh20

real    0m2.287s
user    0m2.340s
sys     0m0.073s
500001

0zsh5zsh10zsh15zsh20

real    0m2.648s
user    0m2.223s
sys     0m0.423s
500001

0bash5bash10bash15bash20

real    0m3.966s
user    0m3.907s
sys     0m0.213s
500001

任意=也许行吗?

尽管如此,这是一个相当随意的测试,但是它确实测试了读取输入,算术评估和变量扩展。可能不全面,但可能在附近。

Teresa e Junior的编辑:@mikeserv和我做过许多其他测试(有关详细信息,请参见我们的聊天记录),我们发现结果可以总结如下:

  • 如果需要速度,请绝对使用破折号,它比任何其他Shell都快得多,比bash快约4倍。
  • 虽然busybox的的外壳可以比慢得多破折号,在一些测试中,它可能会更快,因为它有很多自己的userland工具,像grepsedsort等等,这些不具备的许多功能将常用的GNU实用程序,但可以完成很多工作。
  • 如果速度不是您所关心的全部,那么可以将ksh(或ksh93)视为速度和功能之间的最佳折衷方案。与较小的mksh相比,它的速度要快得多,后者比bash快得多,并且还具有一些独特的功能,例如浮点算法
  • 尽管bash以其简单性,稳定性和功能性而闻名,但在大多数测试中,它是所有shell中最慢的,而且差距很大。

我不能让此代码在bash(以及ksh和zsh)中工作,而只能在dash,mksh和pdksh中工作。我4.2.37(1)-release从Debian和4.2.45(2)-releasePorteus LiveCD(Slackware)尝试过的Bash 。没有null=,而不是输出数字,它的工作方式就像我连续按Return键一样,然后必须使用SIGKILL杀死bash 。
Teresa e Junior

而且我也尝试过bash --posix,但无济于事。
Teresa e Junior

@TeresaeJunior-也许有可能-尽管我不认为它可以使用zshzsh会劫持tty和好,它将启动一个交互式shell。我希望bash会做同样的事情-这就是为什么我要谨慎地仅调用其--posix链接。我可以像大多数人期望的那样做到这一点,但这可能比它的价值还多。您在打电话bash还是在打电话sh
mikeserv

@TeresaeJunior您能来这里发布输出吗?我只是想更好地了解正在发生的事情。
mikeserv

我不应该将答案的文本添加到您的答案的底部以补充它,然后删除我的答案吗?
Teresa e Junior

20

让我们做一个基准。

bash

$ strace -cf bash -c 'for i in $(seq 1 1000); do bash -c ":"; done'

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.12    0.376044         188      2004      1002 wait4
  0.74    0.002805           3      1002           clone
  0.03    0.000130           0      4037           read
  0.03    0.000119           0     15026           rt_sigprocmask
  0.03    0.000096           0     15040      6017 stat
  0.01    0.000055           0      8011           open
  0.01    0.000024           0      5013           getegid
  0.01    0.000021           0     16027           rt_sigaction
  0.00    0.000017           0      9020      5008 access
  0.00    0.000014           0      1001      1001 getpeername
  0.00    0.000013           0      1001           getpgrp
  0.00    0.000012           0      5013           geteuid
  0.00    0.000011           0     15025           mmap
  0.00    0.000011           0      1002           rt_sigreturn
  0.00    0.000000           0         1           write
  0.00    0.000000           0      8017           close
  0.00    0.000000           0      7011           fstat
  0.00    0.000000           0      8012           mprotect
  0.00    0.000000           0      2004           munmap
  0.00    0.000000           0     18049           brk
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         1           dup2
  0.00    0.000000           0      1001           getpid
  0.00    0.000000           0      1002           execve
  0.00    0.000000           0      1001           uname
  0.00    0.000000           0      1001           getrlimit
  0.00    0.000000           0      5013           getuid
  0.00    0.000000           0      5013           getgid
  0.00    0.000000           0      1001           getppid
  0.00    0.000000           0      1002           arch_prctl
  0.00    0.000000           0      1001           time
------ ----------- ----------- --------- --------- ----------------
100.00    0.379372                158353     13028 total

dash

$ strace -cf bash -c 'for i in $(seq 1 1000); do dash -c ":"; done'
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 73.88    0.008543           4      2004      1002 wait4
 25.35    0.002932           3      1002           clone
  0.62    0.000072           0      9026           rt_sigprocmask
  0.10    0.000011           0      1002           rt_sigreturn
  0.05    0.000006           0     15027           rt_sigaction
  0.00    0.000000           0      1037           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0      2011           open
  0.00    0.000000           0      2017           close
  0.00    0.000000           0      2040        17 stat
  0.00    0.000000           0      2011           fstat
  0.00    0.000000           0      8025           mmap
  0.00    0.000000           0      3012           mprotect
  0.00    0.000000           0      1004           munmap
  0.00    0.000000           0      3049           brk
  0.00    0.000000           0      3020      3008 access
  0.00    0.000000           0         1           pipe
  0.00    0.000000           0         1           dup2
  0.00    0.000000           0      1001           getpid
  0.00    0.000000           0         1         1 getpeername
  0.00    0.000000           0      1002           execve
  0.00    0.000000           0         1           uname
  0.00    0.000000           0         1           getrlimit
  0.00    0.000000           0        13           getuid
  0.00    0.000000           0        13           getgid
  0.00    0.000000           0      1013           geteuid
  0.00    0.000000           0        13           getegid
  0.00    0.000000           0      1001           getppid
  0.00    0.000000           0         1           getpgrp
  0.00    0.000000           0      1002           arch_prctl
  0.00    0.000000           0         1           time
------ ----------- ----------- --------- --------- ----------------
100.00    0.011564                 60353      4028 total

每次迭代仅启动一个shell,并且使用no-op运算符-Colon不执行任何操作,然后退出。

结果显示,dash它比bash启动时快得多。dash较小,并且比bash:更少依赖共享库

$ du -s /bin/bash 
956 /bin/bash

$ du -s /bin/dash 
108 /bin/dash

$ ldd /bin/bash
    linux-vdso.so.1 =>  (0x00007fffc7947000)
    libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f5a8110d000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5a80f09000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5a80b7d000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5a81352000)

$ ldd /bin/dash
    linux-vdso.so.1 =>  (0x00007fff56e5a000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb24844c000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb2487f3000)

这是关于启动时间,如何运作。让我们做另一个基准测试:

$ time dash -c 'for i in $(seq 1 1000000);do [ 1 = 1 ];done'

real    0m2.684s
user    0m2.728s
sys     0m0.100s

$ time bash -c 'for i in $(seq 1 1000000);do [ 1 = 1 ];done'

real    0m6.996s
user    0m6.820s
sys     0m0.376s

通过简单的测试1 = 1dash仍然比bash


您的回答很受赞赏,但似乎您只在测量Shell的启动速度,而不是它的运行速度,对吗?
Teresa e Junior

1
@TeresaeJunior:是的,我只提到启动时间。
cuonglm

我认为seq 1 100000应该是seq 1 1000
Mikel 2014年

1
但是对于您的dash测试用例来说,这仅仅是seq 1 1000
Mikel 2014年

抱歉,它是1000用于启动和1000000操作的,已修复。
cuonglm

7

这是经过认证的UNIX(Mac OS X 10.10.3)中各种外壳的启动时间。我重写了测试,使用tcsh来控制循环,以便被测试的外壳不是控制循环的外壳。对于每个外壳,循环在计时之前执行五次,以确保外壳可执行文件和脚本在高速缓存中。

如您所见,没有明确的赢家,但有一个确定的输家。无论如何,bash 4明显比bash 3慢。Dash表现不错,但是鉴于ksh93现在是开源的,没有真正的理由不将其用于所有内容(如果我误解了任何许可方面的知识,我们深表歉意):ksh93快速,可靠,以及UNIX领域(如果不是GNU / Linux领域)的实际标准;它提供了POSIX Shell功能的超集(据我所知,POSIX Shell基于ksh88);它与bash作为交互式shell等效,尽管与tcsh相比落后。失败者当然是zsh。

/bin/bash is v3.2.57(1)
/usr/local/bin/bash is v4.3.33(1)
dash is v0.5.8
ksh is v93u+
mksh is vR50f
pdksh is v5.2.14
/opt/heirloom/5bin/sh is from SysV
yash is v2.37
zsh is v5.0.5

% cat driver.csh 
#!/bin/tcsh

foreach s ( $* )
    echo
    echo "$s"
    foreach i ( `seq 1 5` )
        ./simple_loop.csh "$s"
    end
    /usr/bin/time -p ./simple_loop.csh "$s"
end

% cat simple_loop.csh 
#!/bin/tcsh

set shell = `which ${1}`
foreach i ( `seq 1 1000` )
    ${shell} -c ":"
end

% ./driver.csh /bin/bash /usr/local/bin/bash dash ksh mksh pdksh /opt/heirloom/5bin/sh yash zsh 
/bin/bash
real         4.21
user         1.44
sys          1.94

/usr/local/bin/bash
real         5.45
user         1.44
sys          1.98

dash
real         3.28
user         0.85
sys          1.11

ksh
real         3.48
user         1.35
sys          1.68

mksh
real         3.38
user         0.94
sys          1.14

pdksh
real         3.56
user         0.96
sys          1.17

/opt/heirloom/5bin/sh
real         3.46
user         0.92
sys          1.11

yash
real         3.97
user         1.08
sys          1.44

zsh
real        10.88
user         3.02
sys          5.80

我的结论也是使用ksh93。它是由FSF批准的通用公共许可证。
Teresa e Junior

0

这里有很多答案中有太多不公平的测试案例。如果测试两个外壳,则对每个外壳使用正确的语法。而且在bash中,双括号比单括号快得多并且更可靠,因此根本没有速度差。也可以使用优化的Bashism,然后这些速度差异也就更少了。在我的系统上,bash像地狱般运行,大量使用bashisms。破折号中的posix等价物在这里比较慢。破折号总是比bash快几倍是不正确的。在两者中同时比较posix命令行确实是不公平的,破折号总是最快的。我认为posix已过时。而且在兼容性方面,如今很难找到相关的系统,因为它们没有使用bash shell。

一个很好的比较是:在每个shell中使用最佳命令行,以完成特定的工作。当只有一个shell确实具有优势时,不仅命令行完全相同。这样的比较是不可靠的,并且没有显示出竞争对手的真实表现。我在日常工作中看到,在许多用例中,哪种外壳速度更快。

例如,a要用b字符替换字符串中的所有字符,在bash中可以编写,"${varname//a/b}"而在破折号中则必须调用外部工具,如下所示:"$(echo "$varname" | sed 's/a/b/g')"。如果您必须重复执行数百次-使用bashism可以使速度提高2倍。


3
您是否有任何示例可以用来更新答案,以显示bash如何弥补性能差距甚至在等效任务上更快?使用一些特定的示例,您的答案会更强。
埃里克·雷诺夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.