55

I always thought that the only benefit of using dash instead of bash was that dash was smaller, and therefore many instances of dash would start faster at boot time.

But I have done some research, and found some people migrating all their scripts to dash in the hope they would run faster, and I also found this in the article DashAsBinSh in the Ubuntu Wiki:

The major reason to switch the default shell was efficiency. bash is an excellent full-featured shell appropriate for interactive use; indeed, it is still the default login shell. However, it is rather large and slow to start up and operate by comparison with dash.

Nowadays I've been using lots of bash scripts for many things on my system, and my problem is that I have a particular script that I'm running continuously 24/7, that spawns around 200 children, which together heat my computer 10°C more than in normal usage.

It is a rather large script with lots of bashisms, so porting them to POSIX or some other shell would be very time consuming (and POSIX doesn't really matter for personal use), but it would be worth if I could reduce some of this CPU usage. I know there are also other things to consider, like calling an external binary like sed for a simple bashism like ${foo/bar}, or grep instead of =~.

TL;DR is really bash slower to start up and operate in comparison with dash? Are there other Unix shells which are more efficient than bash?

  • 12
    If you are going to port it for performance, do you think it would be better done in some other language (perl, python, ruby) entirely? They are generally much more efficient, I think, although it will depend on the exact nature of the task. – goldilocks Aug 2 '14 at 13:23
  • Minor point: [ should also be a builtin. – Mikel Aug 2 '14 at 13:48
  • 2
    Keep in mind that unlike you're worried about memory usage the difference mostly shows if you're doing computations in the shell rather than in external programs (ie, you're using the shell the wrong way!); eg on my computer a script using a while loop to count to a million (doing nothing else) is ~2x faster in mksh/zsh and >2x faster in dash, but in a real script I'd offload as much as possible to other programs. – loreb Aug 2 '14 at 17:16
  • 3
    bash used to be very slow. It made a lot of progress recently, but for most things, it is still slower than most other shells. – Stéphane Chazelas Aug 2 '14 at 18:55
  • 1
    Dont use simple bashism's. [ "$foo" != "${foo#*bar}" ] handles your grep thing. And the sed thing: while [ "$foo" != "${foo#*bar}" ]; do s=$s${foo%%bar*} foo=${foo#*bar} ; done ; foo=$s$foo. You can put either thing into a function. – mikeserv Aug 2 '14 at 23:14
37

SHELL SEQ:

Probably a useful means of bench-marking a shell's performance is to do a lot of very small, simple evaluations repetitively. It is important, I think, not just to loop, but to loop over input, because a shell needs to read <&0.

I thought this would complement the tests @cuonglm already posted because it demonstrates a single shell process's performance once invoked, as opposed to his which demonstrates how quickly a shell process loads when invoked. In this way, between us, we cover both sides of the coin.

Here's a function to facilitate the demo:

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

It either increments a variable once per newline read or, as a slight-optimization, if it can, it increments 50 times per newline read. Every time the variable is incremented it is printed to stdout. It behaves a lot like a sort of seq cross nl.

And just to make it very clear what it does - here's some truncated set -x; output after inserting it just before time in the function above:

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

So each shell is first called like:

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

...to generate the input that it will need to loop over when it reads in 3<<\SCRIPT - or when cat does, anyway. And on the other side of that |pipe it calls itself again like:

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

So aside from the initial call to env (because cat is actually called in the previous line); no other processes are invoked from the time it is called until it exits. At least, I hope that's true.

Before the numbers...

I should make some notes on portability.

  • posh doesn't like $((n=n+1)) and insists on $((n=$n+1))

  • mksh doesn't have a printf builtin in most cases. Earlier tests had it lagging a great deal - it was invoking /usr/bin/printf for every run. Hence the echo -n above.

  • maybe more as I remember it...

Anyway, to the numbers:

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

That'll get 'em all in one go...

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

ARBITRARY = MAYBE OK?

Still, this is a rather arbitrary test, but it does test reading input, arithmetic evaluation, and variable expansion. Maybe not comprehensive, but possibly near to there.

EDIT by Teresa e Junior: @mikeserv and I have done many other tests (see our chat for details), and we found the results could be summarized like this:

  • If you need speed, go definitely with dash, it is much faster than any other shell and about 4x faster than bash.
  • While busybox's shell can be much slower than dash, in some tests it could be faster, because it has many of its own userland utilities, like grep, sed, sort, etc., which don't have as many features as the commonly used GNU utilities, but can get the work done as much.
  • If speed is not everything you care about, ksh (or ksh93) can be considered the best compromisse between speed and features. It's speed compares to the smaller mksh, which is way faster than bash, and it has also some unique features, like floating point arithmetic.
  • Although bash is famous for its simplicity, stability, and functionality, it was the slowest of all shells in the majority of our tests, and by a large margin.
  • I can't get this code to work in bash (and also ksh and zsh), only in dash, mksh, and pdksh. Bash I have tried 4.2.37(1)-release from Debian and 4.2.45(2)-release from a Porteus LiveCD (Slackware). Without null=, instead of outputting numbers, it works as if I pressed Return continuously, then I have to kill bash with SIGKILL. – Teresa e Junior Aug 3 '14 at 3:52
  • And I have also tried with bash --posix, to no avail. – Teresa e Junior Aug 3 '14 at 3:57
  • @TeresaeJunior - that maybe possible - though I don't believe it will work with zsh. zsh will hijack the tty and well, it will launch an interactive shell. I expect bash will do the same - which is why I am careful to only call its --posix link. I can make it do as you expect for most any of them, but it may be more work than its worth. Are you calling bash or are you calling sh? – mikeserv Aug 3 '14 at 3:58
  • @TeresaeJunior Can you come in here and post the output? I'd just like to get a better idea of what is happening. – mikeserv Aug 3 '14 at 4:01
  • Shouldn't I add the text of my answer to the bottom of yours, to complement it, and then delete mine? – Teresa e Junior Aug 4 '14 at 0:13
19
+50

Let do a benchmark.

With 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

With 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

Each iteration only start a shell and do nothing with no-op operator - colon, then quit.

As the result show, dash is extremely faster than bash at startup. dash is smaller, and depend on less shared library than 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)

This is about startup time, how about operate. Let do another benchmark:

$ 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

With simple test 1 = 1, dash still much faster than bash.

  • Your answer is very appreciated, but it seems you are measuring only how fast is the shell to startup, not really how fast it operates, right? – Teresa e Junior Aug 3 '14 at 3:33
  • 1
    @TeresaeJunior: Yes, I mention about startup time only. – cuonglm Aug 3 '14 at 6:13
  • I assume seq 1 100000 should be seq 1 1000? – Mikel Aug 3 '14 at 16:31
  • 1
    But for your dash test case it's only seq 1 1000? – Mikel Aug 3 '14 at 18:24
  • Oh,sorry, it's 1000 for startup and 1000000 for operate, fixed. – cuonglm Aug 3 '14 at 18:28
7

Here's some startup timings of various shells in a certified UNIX (Mac OS X 10.10.3). I rewrote the test to use tcsh to control the loops so that the shell being tested was not the one controlling the loops. For each shell the loop is executed five times prior to timing, to ensure that the shell executable and the scripts are in cache.

As you can see, there's no clear-cut winner, but there's one definitive loser. Anyhow, bash 4 is distinctly slower than bash 3. Dash performs well, but given that ksh93 is now open-source, there's no real reason not to use it for everything (apologies if I misunderstand any licensing niceties): ksh93 is fast, solid, and a de-facto standard in UNIX-land (if not in GNU/Linux-land); it provides a superset of the POSIX shell functionality (as far as I understand, the POSIX shell was based on ksh88); it is equal to bash as an interactive shell, though lagging compared to tcsh. And the loser is of course 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
  • My conclusion was using ksh93 too. It is under the Common Public License, which has been approved by the FSF. – Teresa e Junior Jun 1 '15 at 13:29
0

There are too many unfair test cases in many answers here. If test two shells then use the correct syntax for each of them. And in bash doublebrackets are much faster and more reliable than singlebrackets, so there is a much less speed difference at all. Also use optimized bashisms and then these speed differences are more less too. On my system bash runs like hell, with heavy use of bashisms. And posix equivalents in dash are slower here. This is not correct that dash is always multiple times faster than bash. Really it is pretty unfair to compare posix command lines in both, who dash can always be the fastest. In my view posix is heavy outdated. And in terms of compatibility, it is really hard to found relevant systems nowdays, they didn't use a bash shell.

A good comparison is: to use the best possible command line in each shell, to finish a specific job. Not only exactly the same command line, when only one shell really has an advantage here. Comparisons like this are unreliable and didn't show the real performance of the competitors. I see at my every day job, which shell is faster in many use cases.

For example, to replace all a characters in string with b characters, in bash you can write "${varname//a/b}" while in dash you have to call external tool like this: "$(echo "$varname" | sed 's/a/b/g')". If you have to repeat it few hundred times - using bashism can give you 2x speedup.

  • 3
    Do you have any examples you could update your answer with to show how bash can close the performance gap or even be faster on the equivalent tasks? Your answer would be a lot stronger with some specific examples. – Eric Renouf Jun 10 '17 at 11:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.