Is dash or some other shell "faster" than bash?
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 aprintf
builtin in most cases. Earlier tests had it lagging a great deal - it was invoking/usr/bin/printf
for every run. Hence theecho -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.
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
.
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