Jak zmierzyć czas wykonania programu i zapisać go w zmiennej

61

Aby dowiedzieć się, jak długo trwają niektóre operacje w skrypcie Bash (v4 +), chciałbym przeanalizować dane wyjściowe timepolecenia „osobno” i (ostatecznie) przechwycić je w zmiennej Bash ( let VARNAME=...).

Teraz używam time -f '%e' ...(a raczej command time -f '%e' ...ze względu na wbudowane Bash), ale ponieważ już przekierowałem dane wyjściowe wykonanego polecenia, jestem naprawdę zagubiony w kwestii sposobu przechwycenia wyniku timepolecenia. Zasadniczo problemem jest oddzielenie wyjście z timez wyjścia wykonywanego polecenia (ów).

Potrzebuję funkcji zliczania czasu w sekundach (liczbach całkowitych) między uruchomieniem polecenia a jego zakończeniem. Nie musi to być timepolecenie ani odpowiednie wbudowane.


Edycja: biorąc pod uwagę dwie przydatne odpowiedzi poniżej, chciałem dodać dwa wyjaśnienia.

  1. Nie chcę wyrzucać danych wyjściowych wykonanego polecenia, ale tak naprawdę nie będzie miało znaczenia, czy zakończy się na stdout, czy stderr.
  2. Wolałbym podejście bezpośrednie niż pośrednie (tzn. Przechwytywanie danych wyjściowych w przeciwieństwie do przechowywania ich w plikach pośrednich).

Dotychczasowe rozwiązanie datezbliża się do tego, czego chcę.

0xC0000022L
źródło
Najbardziej bezpośrednim sposobem na uzyskanie danych i obsługę ich przy jednoczesnym umożliwieniu normalnego działania byłoby zrobienie tego w programie C przy użyciu fork(), execvp()i wait3()/wait4(). To jest właśnie czas i przyjaciele. Nie znam prostego sposobu na zrobienie tego w bash / perl bez przekierowywania do pliku lub podobnego podejścia.
penguin359,
Jest powiązany pytanie, które może się okazać interesujące tutaj dzieje .
Caleb
@Caleb: dzięki. Rzeczywiście interesujące. Jednak dla moich celów wystarczy to zrobić w skrypcie.
0xC0000022L

Odpowiedzi:

85

Aby uzyskać wyjście timezmiennej var, użyj:

usr@srv $ mytime="$(time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$mytime"

real    0m0.006s
user    0m0.001s
sys     0m0.005s

Możesz także poprosić o pojedynczy typ czasu, np. Utime:

usr@srv $ utime="$( TIMEFORMAT='%lU';time ( ls ) 2>&1 1>/dev/null )"
usr@srv $ echo "$utime"
0m0.000s

Aby uzyskać czas, którego możesz użyć date +%s.%N, więc weź go przed i po wykonaniu i oblicz różnicę:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
binfalse
źródło
4
Nie chciałem jednak wyrzucać danych wyjściowych z polecenia. Sądzę więc, że twój trzeci blok kodu jest najbliższy temu, co miałem na myśli. Chociaż ostatni napisałbym jako DIFF=$((END-START)), korzystając z wyrażeń arytmetycznych. :) ... Dziękuję za odpowiedź. +1
0xC0000022L
1
@STATUS_ACCESS_DENIED: Bash nie wykonuje arytmetyki zmiennoprzecinkowej, więc jeśli chcesz mieć lepszą rozdzielczość niż druga ( .%Nw kodzie binfalse), potrzebujesz bcalbo bardziej wyszukanych obliczeń.
Gilles
@Gilles: Wiem. Jak napisałem w moim pytaniu, liczby całkowite są w porządku. Nie musisz mieć wyższej rozdzielczości niż sekunda. Ale dzięki, wywołanie daty musiałoby zostać zmienione. Ale zdałem sobie z tego sprawę.
0xC0000022L
6
Do Twojej wiadomości, formatowanie daty% N wydaje się nie działać na Mac OS X, po prostu zwraca „N”. Ok na Ubuntu.
David,
Zauważ, że chociaż time (cmd) 2> somethingprace nad przekierowaniem wyjścia timingu do file, nie mają (zgodnie z dokumentacją), nie znajdują się w innych powłokach, w których timeznajduje się słowo kluczowe i mogą być uważane za błąd . Nie polegałbym na tym, ponieważ może nie działać w przyszłych wersjach bash.
Stéphane Chazelas
17

W bashu wynik wyjściowy timekonstrukcji przechodzi w błąd standardowy i można przekierować standardowy błąd potoku, na który wpływa. Więc zacznijmy od komendy, która pisze do swoich wyjściowych i błędów streamas: sh -c 'echo out; echo 1>&2 err'. Aby nie pomieszać strumienia błędów polecenia z danymi wyjściowymi time, możemy tymczasowo przekierować strumień błędów polecenia na inny deskryptor pliku:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; }

Zapisuje to outna fd 1, errna fd 3, a czasy na fd 2:

{ time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } \
    3> >(sed 's/^/ERR:/') 2> >(sed 's/^/TIME:/') > >(sed 's/^/OUT:/')

Przyjemniej byłoby mieć errna fd 2 i czasy na fd 3, więc zamieniamy je, co jest uciążliwe, ponieważ nie ma bezpośredniego sposobu na zamianę dwóch deskryptorów plików:

{ { { time -p sh -c 'echo out; echo 1>&2 err' 2>&3; } 3>&2 2>&4; } 4>&3; } 3> >(sed 's/^/TIME:/') 2> >(sed 's/^/ERR:/') > >(sed 's/^/OUT:/')

To pokazuje, jak możesz przetworzyć dane wyjściowe polecenia, ale jeśli chcesz uchwycić zarówno wynik polecenia, jak i jego czasy, musisz ciężko pracować. Jednym z rozwiązań jest użycie pliku tymczasowego. W rzeczywistości jest to jedyne niezawodne rozwiązanie, jeśli chcesz uchwycić zarówno standardowy błąd polecenia, jak i standardowe wyjście. Ale w przeciwnym razie możesz uchwycić cały wynik i skorzystać z faktu, że timema on przewidywalny format (jeśli używasz, time -paby uzyskać format POSIX lub TIMEFORMATzmienną specyficzną dla bash ).

nl=$'\n'
output=$(TIMEFORMAT='%R %U %S %P'; mycommand)
set ${output##*$nl}; real_time=$1 user_time=$2 system_time=$3 cpu_percent=$4
output=${output%$nl*}

Jeśli zależy Ci tylko na czasie naściennym, bieganie dateprzed i po jest prostym rozwiązaniem (jeśli jest nieco bardziej nieprecyzyjne ze względu na dodatkowy czas spędzony na ładowaniu polecenia zewnętrznego).

Gilles
źródło
Wow, dużo do przetestowania. Wielkie dzięki za obszerną odpowiedź. +1.
0xC0000022L
7

Z czasem wyjście komendy wychodzi na standardowe wyjście, a czas wychodzi na standardowe wyjście. Aby je rozdzielić, możesz:

command time -f '%e' [command] 1>[command output file] 2>[time output file]

Ale teraz czas jest w pliku. Nie sądzę, że Bash jest w stanie bezpośrednio wstawić stderr do zmiennej. Jeśli nie masz nic przeciwko przekierowaniu gdzieś polecenia, możesz:

FOO=$((( command time -f '%e' [command]; ) 1>outputfile; ) 2>&1; )

Gdy to zrobisz, wyjście polecenia zostanie włączone i upłynie outputfileczas potrzebny do uruchomienia $FOO.

marinus
źródło
1
ooh, nie zdawałem sobie sprawy. Wiedziałem, że timepisze do stderr, ale nie zdawałem sobie sprawy, że połączy stdout i stderr wykonanego polecenia z jego stdout. Ale generalnie nie ma bezpośredniej metody (tj. Bez pliku pośredniego)? +1.
0xC0000022L
3
@STATUS_ACCESS_DENIED: timenie scala poleceń stdouti stderr. Sposób, w jaki pokazałem, zakładał, że potrzebujesz tylko wyjścia standardowego polecenia. Ponieważ bashbędziesz przechowywać tylko to, co z niego wyjdzie stdout, musisz przekierować. W przypadku, gdy możesz bezpiecznie upuścić stderr polecenia: czas zawsze będzie na ostatniej linii stderr. Jeśli potrzebujesz obu strumieni wyjściowych polecenia, sugeruję zawinięcie go w inny skrypt.
marinus
6

Jeśli jesteś w bash(i nie sh) i NIE potrzebujesz dokładności poniżej sekundy, możesz datecałkowicie pominąć wywołanie i zrobić to bez tworzenia dodatkowych procesów, bez konieczności oddzielania połączonych danych wyjściowych i bez przechwytywania i analizowania danych wyjściowych z dowolnego polecenia:

# SECONDS is a bash special variable that returns the seconds since set.
SECONDS=0
mycmd <infile >outfile 2>errfile
DURATION_IN_SECONDS=$SECONDS
# Now `$DURATION_IN_SECONDS` is the number of seconds.
sanpath
źródło
Miły. Nigdy wcześniej nie słyszałem o
SEKUNDACH
Czy wiesz, od której wersji Bash została wprowadzona?
0xC0000022L
4

W tym celu prawdopodobnie lepiej jest użyć times(niż time), w bashktórym drukuje skumulowane czasy użytkownika i systemu dla powłoki i dla procesów uruchamianych z powłoki, na przykład:

$ (sleep 2; times) | (read tuser tsys; echo $tuser:$tsys)
0m0.001s:0m0.003s

Zobacz: help -m timesaby uzyskać więcej informacji.

kenorb
źródło
4

W łączeniu wszystkich poprzednich odpowiedzi w OSX

ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31+

możesz zrobić jak

microtime() {
    python -c 'import time; print time.time()'
}
compute() {
    local START=$(microtime)
    #$1 is command $2 are args
    local END=$(microtime)
    DIFF=$(echo "$END - $START" | bc)
    echo "$1\t$2\t$DIFF"
}
loretoparisi
źródło
1

Zainstaluj /bin/time(np. pacman -S time)

Więc zamiast błędu przy próbie -fflagi:

$ time -f %e sleep 0.5
bash: -f: command not found

real    0m0.001s
user    0m0.001s
sys     0m0.001s

Możesz go właściwie użyć:

$ /bin/time -f %e sleep 0.5
0.50

I uzyskaj to, czego chcesz - czas w zmiennej (przykład wykorzystuje %edla rzeczywistego czasu, który upłynął, w celu sprawdzenia innych opcji man time):

#!/bin/bash
tmpf="$(mktemp)"
/bin/time -f %e -o "$tmpf" sleep 0.5
variable="$(cat "$tmpf")"
rm "$tmpf"

echo "$variable"
Grzegorz Wierzowiecki
źródło
/usr/bin/timena wielu systemach
Basile Starynkevitch,
Jeśli przyjrzysz się uważnie, wskazałem to w moim pytaniu. timejest powłoką wbudowaną w Bash (i przypuszczalnie inne powłoki), ale dopóki jest ścieżka do timepliku wykonywalnego PATH, możemy użyć, command timeaby upewnić się, że uruchamiamy zewnętrzne polecenie, a nie wbudowane.
0xC0000022L,
1

Tak samo, jak podczas pojedynku z powyższymi stwierdzeniami, szczególnie biorąc pod uwagę odpowiedź Grzegorza. Byłem zaskoczony, widząc na moim systemie Ubuntu 16.04 Xenial te dwa wyniki:

$ which time
/usr/bin/time

$ time -f %e sleep 4
-f: command not found
real    0m0.071s
user    0m0.056s
sys     0m0.012s

$ /usr/bin/time -f %e sleep 4
4.00

Nie mam ustawionych żadnych aliasów, więc nie wiem, dlaczego tak się dzieje.

Paul Pritchard
źródło
1
timejest także wbudowaną powłoką.
Basile Starynkevitch,
Jeśli chcesz się upewnić, że uruchomiłeś polecenie, użyj command, jeśli chcesz mieć pewność, że uruchomiłeś wbudowane, użyj builtin. Tu nie ma magii. Użyj, helpaby dowiedzieć się, jakie są dostępne polecenia lub type <command>dowiedzieć się, jaki <command>jest typ (np. Wbudowane, zewnętrzne polecenie, funkcja lub alias).
0xC0000022L,
Basile, masz całkowitą rację. Nie zauważyłem znaczenia commandpolecenia. Jest to faktem, że wywoływane jest / usr / bin / time. Oznacza to, że następujące dwa stwierdzenia są kompatybilne: $ command time -f %e sleep 4i$ /usr/bin/time -f %e sleep
Paul Pritchard
0

spróbuj tego, uruchamia proste polecenie z argumentami i podaje czasy $ real $ user $ sys i zachowuje kod wyjścia.

Nie rozwidla również podpowłok ani nie depcze żadnych zmiennych poza prawdziwym systemem użytkownika i w żaden inny sposób nie zakłóca działania skryptu

timer () {
  { time { "$@" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $?
  read -d "" _ real _ user _ sys _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

na przykład

  timer find /bin /sbin /usr rm /tmp/
  echo $real $user $sys

Uwaga: tylko proste polecenie nie jest potokiem, którego komponenty są uruchamiane w podpowłoce

Ta wersja pozwala określić jako 1 $ nazwę zmiennych, które powinny otrzymać 3 razy:

timer () {
  { time { "${@:4}" ; } 2>${_} {_}>&- ; } {_}>&2 2>"/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  set -- $? "$@"
  read -d "" _ "$2" _ "$3" _ "$4" _ < "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  rm -f "/tmp/$$.$BASHPID.${#FUNCNAME[@]}"
  return $1
}

na przykład

  timer r u s find /bin /sbin /usr rm /tmp/
  echo $r $u $s

i może być przydatny, jeśli zostanie wywołany rekurencyjnie, aby uniknąć deptania na czas; ale wtedy rus itp. należy uznać za lokalne w ich użyciu.

http://blog.sam.liddicott.com/2016/01/timeing-bash-commands.html

Sam Liddicott
źródło