12

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
agc
  • 7,045
  • 3
  • 23
  • 53
  • 6
    Just use `/usr/bin/time` ? – Kusalananda Jan 01 '17 at 17:57
  • Concerning the portability, how about simply subtracting before and after value of `/proc/uptime`? (Also works across time changes.) – phk Jan 01 '17 at 17:59
  • 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 01 '17 at 21:48
  • @MichaelHomer, if you know, (or believe), that certain constraints are necessarilly mutually exclusive, please put post those impossible combos in an answer. – agc Jan 01 '17 at 22:11
  • 1
    That's not an answer to the question, it's a prompt for you to clarify what you want. – Michael Homer Jan 01 '17 at 22:13
  • @MichaelHomer, various ways I'd suppose. For example, here's a crude method: a util might *on the fly* compute a good average of how much time a given shell takes to load with a null script, then subtract that time from the time the same shell takes to run the user's test script. – agc Jan 01 '17 at 22:32
  • 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 01 '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 01 '17 at 23:47
  • @Gilles, just curious, various problems come up now and then, it's not one thing. See my comment to [*muru*'s answer](http://unix.stackexchange.com/a/334153/165517) for an example of inconsistency among `time` *builtin*s... – agc Jan 01 '17 at 23:53
  • Also you have a reopen vote regarding your question. – peterh Dec 16 '17 at 05:07
  • Both bash and dash are loaded _on demand_ (so were they in 2017 ;-). Your assumption that they will have to be read whole from disk before being started, and starting a big executable is necessarily slower than a fast one is _completely wrong_. Especially since they're both dynamically linked, and the size of the libraries they're using may be bigger than their own size. –  Nov 30 '19 at 16:00
  • 1
    s/than a fast one/than a small one/ above –  Dec 01 '19 at 08:05
  • @mosvy, Well that's commonly true, but it's generally incorrect -- it all depends on the environment. An embedded (or a [minimalist system](https://en.wikipedia.org/wiki/Category:Floppy-based_Linux_distributions)) system might have a minimal amount of memory, and not preload anything because it hasn't enough buffer memory. – agc Dec 03 '19 at 18:03
  • Nothing to do with "preload": demand loading means that only the stuff _actually used_ from the executable will be read from the disk: you can have a huge multi-giga executable, and have it start and exit instantly, because 99% of it will never be paged-in. –  Dec 03 '19 at 18:05
  • @mosvy, And of course [not all shells are dynamically linked](https://en.wikipedia.org/wiki/Stand-alone_shell). – agc Dec 03 '19 at 18:06
  • `bash` and `dash` are both dynamically linked. –  Dec 03 '19 at 18:07

5 Answers5

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
agc
  • 7,045
  • 3
  • 23
  • 53
muru
  • 69,900
  • 13
  • 192
  • 292
  • 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 01 '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 01 '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 Command exited with non-zero status 127 "* errors. – agc Jan 01 '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 01 '17 at 18:50
  • @muru, what's the source of that POSIX quote? – agc Jan 01 '17 at 20:00
  • 1
    @agc http://manpages.ubuntu.com/manpages/wily/en/man1/time.1posix.html, section RATIONALE – muru Jan 01 '17 at 20:01
  • @muru, Reviewing *RATIONALE*: that section just seems to describe two sides of a compromise made because apparently nobody at the time which that was written had arrived at a satisfactory answer to this very question. Rather *POSIX* settled for both shell-based `time` builtins and a external `time` util, because even two non-general `time` methods were better than no `time` standards at all. – agc Dec 17 '17 at 04:37
8

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.

agc
  • 7,045
  • 3
  • 23
  • 53
Rabin
  • 3,818
  • 1
  • 21
  • 23
  • 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 01 '17 at 18:21
  • 1
    You should add that this requires GNU `date` specifically. – Kusalananda Jan 01 '17 at 18:21
  • @phk please explain ? – Rabin Jan 01 '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 01 '17 at 18:30
  • @phk Thanks, you are right, small change but would be considered. – Rabin Jan 01 '17 at 18:34
  • 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 01 '17 at 19:44
  • @JörgWMittag This wasn't about the leap second which do not affect UNIX epoch timestamps AFAIK and while the adaption method you talk about would make sense it is not what I was seeing. I believe the client was `busybox`'s `ntpd`. – phk Jan 01 '17 at 23:12
  • NTP clients will prefer slowing/accelerating the clock if the time is slightly off instead of changing it directly. This prevents jumps and clock discontinuities, which is generally nicer to applications working with timestamps. However, leap seconds are handled as additional seconds, unless you are using googles smearing NTP servers. – Jonas Schäfer Jan 02 '17 at 15:08
7

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'
Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • How does this "eliminate the time taken for each shell to load and initialize itself"? – Michael Homer Jan 01 '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 01 '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 01 '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 https://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.

Michael Homer
  • 74,824
  • 17
  • 212
  • 233
  • Coincidentally, just before reading this, (and the last line about `eval`), I was tweaking [the script in *Rabin*'s answer](http://unix.stackexchange.com/a/334152/165517) to include `eval "$@"`, so it could run shell *builtins* on the fly. – agc Jan 01 '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.

phk
  • 5,893
  • 7
  • 41
  • 70
  • 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 01 '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 05: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