10

For comparing run times of scripts between different shells, some SE answers suggest using bash's built-in time command, like so:

time bash -c 'foo.sh'
time dash -c 'foo.sh'

...etc, for every shell to test. Such benchmarks fail to eliminate the time taken for each shell to load and initialize itself. For example, suppose both of the above commands were stored on a slow device with the read speed of an early floppy disk, (124KB/s), dash (a ~150K executable) would load about 7x faster than bash (~1M), the shell loading time would skew the time numbers -- the pre-loading times of those shells being irrelevant to measuring the run times of foo.sh under each shell after the shells were loaded.

What's the best portable and general util to run for script timing that can be run from within each shell? So the above code would look something like:

bash -c 'general_timer_util foo.sh'
dash -c 'general_timer_util foo.sh'

NB: no shell built-in time commands, since none are portable or general.


Better yet if the util is also able to benchmark the time taken by a shell's internal commands and pipelines, without the user having to first wrap them in a script. Artificial syntax like this would help:

general_timer_util "while read x ; do echo x ; done < foo"

Some shells' time can manage this. For example bash -c "time while false ; do : ; done" works. To see what works, (and doesn't), on your system try:

tail +2 /etc/shells | 
while read s ; do 
    echo $s ; $s -c "time while false ; do : ; done" ; echo ----
done
  • 6
    Just use /usr/bin/time ? – Kusalananda Jan 1 '17 at 17:57
  • 1
    I don't understand how any non-builtin could possibly both "eliminate the time taken for each shell to load and initialize itself" and execute a standalone script while being "portable and general". – Michael Homer Jan 1 '17 at 21:48
  • 1
    That's not an answer to the question, it's a prompt for you to clarify what you want. – Michael Homer Jan 1 '17 at 22:13
  • 1
    I've posted my best attempt, but I think the question is still underspecified about exactly what it's actually trying to achieve. – Michael Homer Jan 1 '17 at 22:48
  • 2
    What do you mean by “portable or general”? The shell builtins are as portable (work on as many systems) and more general (work in more circumstances, as they can time something other than the execution of a file) as the external command. What problem are you trying to solve? – Gilles 'SO- stop being evil' Jan 1 '17 at 23:47
10

You should note that time is specified by POSIX, and AFAICT the only option that POSIX mentions (-p) is supported correctly by various shells:

$ bash -c 'time -p echo'

real 0.00
user 0.00
sys 0.00
$ dash -c 'time -p echo'

real 0.01
user 0.00
sys 0.00
$ busybox sh -c 'time -p echo'

real 0.00
user 0.00
sys 0.00
$ ksh -c 'time -p echo'       

real 0.00
user 0.00
sys 0.00
  • 1
    The problem is that to be able to compare timings, the result has to be timed by the same implementation of time. It's comparable to letting sprinters measure their own time on 100m, individually, instead of doing it with one clock at the same time. This is obviously nitpicking, but still... – Kusalananda Jan 1 '17 at 18:25
  • @Kusalananda I thought the problem was that OP thought time isn't portable; it seems to be portable. (I agree with your point on comparability, though) – muru Jan 1 '17 at 18:26
  • @muru, on my system dash -c 'time -p while false ; do : ; done' returns "time: cannot run while: No such file or directory<cr> Command exited with non-zero status 127 " errors. – agc Jan 1 '17 at 18:46
  • 1
    @agc POSIX also says: "The term utility is used, rather than command, to highlight the fact that shell compound commands, pipelines, special built-ins, and so on, cannot be used directly. However, utility includes user application programs and shell scripts, not just the standard utilities." (see section RATIONALE) – muru Jan 1 '17 at 18:50
  • 1
    @agc manpages.ubuntu.com/manpages/wily/en/man1/time.1posix.html, section RATIONALE – muru Jan 1 '17 at 20:01
7

I use the GNU date command, which supports a high resolution timer:

START=$(date +%s.%N)
# do something #######################

"$@" &> /dev/null

#######################################
END=$(date +%s.%N)
DIFF=$( echo "scale=3; (${END} - ${START})*1000/1" | bc )
echo "${DIFF}"

And then I call the script like this:

/usr/local/bin/timing dig +short unix.stackexchange.com
141.835

The output unit is in milliseconds.

  • 1
    Assuming the time (epoch time) does not change in-between. Can't think of a case in practise where it would cause a problem but still worth mentioning. – phk Jan 1 '17 at 18:21
  • 1
    You should add that this requires GNU date specifically. – Kusalananda Jan 1 '17 at 18:21
  • @phk please explain ? – Rabin Jan 1 '17 at 18:26
  • 1
    @Rabin Let's say your NTP client issues and update and changes your clock between where START and END is set, then this would obviously affect your result. No idea how precise you need it and whether it matters in your case though but like I said, something to keep in mind. (Fun story: I know a software where exactly it led to an unexpectedly negative results – it was used to throughput calculations – which then broke a few things.) – phk Jan 1 '17 at 18:30
  • 1
    Also, don't some NTP clients slow down and speed up the clock, rather than making "jumps" in the system time? If you have an NTP client like that, and you made some timings yesterday evening, they are probably skewed by the NTP client "anticipating" the leap second. (Or does the system clock simply run to 61 in that case?) – Jörg W Mittag Jan 1 '17 at 19:44
6

The time utility is usually built into the shell, as you have noticed, which makes it useless as a "neutral" timer.

However, the utility is usually also available as an external utility, /usr/bin/time, that may well be used to perform the timing experiments that you propose.

$ bash -c '/usr/bin/time foo.sh'
  • How does this "eliminate the time taken for each shell to load and initialize itself"? – Michael Homer Jan 1 '17 at 21:45
  • 1
    If foo.sh is executable and has a shebang, then this always runs it with the same shell, and it does count the startup time of that shell, so this is not what OP wants. If foo.sh is missing one of those, then this does not work at all. – Kevin Jan 1 '17 at 21:48
  • @Kevin Very true. I only took "no shell built-in time" into consideration it seems. The shell startup time might have to be measured separately. – Kusalananda Jan 1 '17 at 21:58
  • 1
    I don't know of any shell that has a time builtin command. However many shells including bash have a time keyword that can be used to time pipelines. To disable that keyword so the time command (in the file system) be used, you can quote it like "time" foo.sh. See also unix.stackexchange.com/search?q=user%3A22565+time+keyword – Stéphane Chazelas Jan 20 '17 at 16:22
6

Here is a solution that:

  1. eliminate[s] the time taken for each shell to load and initialize itself

  2. can be run from within each shell

  3. Uses

    no shell built-in time commands, since none are portable or general

  4. Works in all POSIX-compatible shells.
  5. Works on all POSIX-compatible and XSI-conforming systems with a C compiler, or where you can compile a C executable in advance.
  6. Uses the same timing implementation on all shells.

There are two parts: a short C program that wraps up gettimeofday, which is deprecated but still more portable than clock_gettime, and a short shell script that uses that program to get a microsecond-precision clock reading both sides of sourcing a script. The C program is the only portable and minimal-overhead way to get a sub-second precision on a timestamp.

Here is the C program epoch.c:

#include <sys/time.h>
#include <stdio.h>
int main(int argc, char **argv) {
    struct timeval time;
    gettimeofday(&time, NULL);
    printf("%li.%06i", time.tv_sec, time.tv_usec);
}

And the shell script timer:

#!/bin/echo Run this in the shell you want to test

START=$(./epoch)
. "$1"
END=$(./epoch)
echo "$END - $START" | bc

This is standard shell command language and bc and should work as a script under any POSIX-compatible shell.

You can use this as:

$ bash timer ./test.sh
.002052
$ dash timer ./test.sh
.000895
$ zsh timer ./test.sh
.000662

It doesn't measure system or user time, only non-monotonic wall-clock elapsed time. If the system clock changes during the execution of the script, this will give incorrect results. If the system is under load, the result will be unreliable. I don't think anything better can be portable between shells.

A modified timer script could use eval instead to run commands outside of a script.

  • Coincidentally, just before reading this, (and the last line about eval), I was tweaking the script in Rabin's answer to include eval "$@", so it could run shell builtins on the fly. – agc Jan 1 '17 at 23:03
4

Multiple times revised solution using /proc/uptime and dc/bc/awk in large parts thanks to the input by agc:

#!/bin/sh

read -r before _ < /proc/uptime

sleep 2s # do something...

read -r after _ < /proc/uptime

duration=$(dc -e "${after} ${before} - n")
# Alternative using bc:
#   duration=$(echo "${after} - ${before}" | bc)
# Alternative using awk:
#   duration=$(echo "${after} ${before}" | awk '{print $1 - $2}')

echo "It took $duration seconds."

Assumes obviously that /proc/uptime exists and has a certain form.

  • 3
    This would make it portable between shells, but not portable between Unix implementations since some simply lack the /proc filesystem. If this is a concern or not, I don't know. – Kusalananda Jan 1 '17 at 18:15
  • 1
    For emphasis this Q suggests floppy disk speeds or worse. In which case the overhead of loading awk can be significant. Maybe b=$(cat /proc/uptime) before, then a=$(cat /proc/uptime) after, then parse $a and $b and subtract. – agc Dec 17 '17 at 5:36
  • @agc Good input, thanks, I added some alternative solutions accordingly. – phk Dec 17 '17 at 10:23
  • 1
    Didn't think of it before, but if builtins like read are better than cat, this would be cleaner (and a little faster): read before dummyvar < /proc/uptime ;sleep 2s;read after dummyvar < /proc/uptime; duration=$(dc -e "${after} ${before} - n");echo "It took $duration seconds." – agc Dec 17 '17 at 19:52

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