Come misurare il tempo di esecuzione del programma e memorizzarlo all'interno di una variabile


61

Per scoprire quanto tempo impiegano determinate operazioni all'interno di uno script Bash (v4 +), vorrei analizzare l'output dal timecomando "separatamente" e (in definitiva) catturarlo all'interno di una variabile Bash ( let VARNAME=...).

Ora sto usando time -f '%e' ...(o piuttosto a command time -f '%e' ...causa del built-in di Bash), ma poiché ho già reindirizzato l'output del comando eseguito, sono davvero perso su come farei per acquisire l'output del timecomando. Fondamentalmente il problema qui è quello di separare l'output di timedall'output dei comandi eseguiti.

Quello che voglio è la funzionalità di contare la quantità di tempo in secondi (numeri interi) tra l'avvio di un comando e il suo completamento. Non deve essere il timecomando o il rispettivo built-in.


Modifica: date le due utili risposte di seguito, volevo aggiungere due chiarimenti.

  1. Non voglio buttare via l'output del comando eseguito, ma non importa se finisce su stdout o stderr.
  2. Preferirei un approccio diretto rispetto a uno indiretto (ovvero catturare l'output direttamente anziché memorizzarlo in file intermedi).

La soluzione datefinora utilizzata si chiude a ciò che voglio.


Il modo più diretto per ottenere i dati e gestirli pur continuando a eseguirli normalmente sarebbe farlo in un programma C usando fork(), execvp()e wait3()/wait4(). Questo è in definitiva ciò che il tempo e gli amici stanno facendo. Non sono a conoscenza di un modo semplice per farlo in bash / perl senza reindirizzare a un file o un approccio simile.
penguin359,

C'è una domanda correlata che potresti trovare interessante succedendo qui .
Caleb,

@Caleb: grazie. Davvero interessante. Tuttavia, per i miei scopi è sufficiente farlo in uno script.
0xC0000022L

Risposte:


85

Per ottenere l'output di timein un var utilizzare quanto segue:

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

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

Puoi anche chiedere un solo tipo di orario, ad esempio utime:

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

Per ottenere il tempo puoi anche usare date +%s.%N, quindi prendilo prima e dopo l'esecuzione e calcola il diff:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF

4
Tuttavia, non volevo eliminare l'output dal comando. Quindi immagino che il tuo terzo blocco di codice sia il più vicino a quello che avevo in mente. Anche se scriverei l'ultimo come DIFF=$((END-START)), facendo uso delle espressioni aritmetiche. :) ... Grazie per la risposta. +1
0xC0000022L

1
@STATUS_ACCESS_DENIED: Bash non esegue l'aritmetica in virgola mobile, quindi se vuoi una risoluzione migliore della seconda ( .%Nnel codice di binfalse), hai bisogno bco calcoli più elaborati.
Gilles 'SO- smetti di essere malvagio' il

@Gilles: lo so. Come ho scritto nella mia domanda, gli interi vanno bene. Non è necessario disporre di una risoluzione superiore alla seconda. Ma grazie, l'invocazione della data dovrebbe essere cambiata. L'avevo capito, però.
0xC0000022L

6
Cordiali saluti, la formattazione della data% N non sembra funzionare su Mac OS X, restituisce semplicemente "N". Ok su Ubuntu.
David,

Si noti che mentre il time (cmd) 2> somethingreindirizzamento dell'output di temporizzazione funziona file, non è previsto (come da documentazione), non in altre shell in cui timeè presente una parola chiave e potrebbe essere considerato un bug . Non farei affidamento su di esso poiché potrebbe non funzionare nelle versioni future di bash.
Stéphane Chazelas,

17

In bash, l'output del timecostrutto va al suo errore standard e puoi reindirizzare l'errore standard della pipeline che influenza. Quindi cominciamo con un comando che scrive ai suoi streamas di uscita e di errore: sh -c 'echo out; echo 1>&2 err'. Per non confondere il flusso di errori del comando con l'output di time, possiamo deviare temporaneamente il flusso di errori del comando su un descrittore di file diverso:

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

Questo scrive outin fd 1, errin fd 3 e i tempi in 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:/')

Sarebbe più piacevole avere errsu fd 2 e i tempi su fd 3, quindi li scambiamo, il che è scomodo perché non esiste un modo diretto per scambiare due descrittori di file:

{ { { 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:/')

Questo mostra come è possibile postelaborare l'output del comando, ma se si desidera acquisire sia l'output del comando sia i relativi tempi, è necessario lavorare di più. L'uso di un file temporaneo è una soluzione. In effetti, è l'unica soluzione affidabile se è necessario acquisire sia l'errore standard del comando sia l'output standard. In caso contrario, è possibile acquisire l'intero output e trarre vantaggio dal fatto che timeha un formato prevedibile (se si utilizza time -pper ottenere il formato POSIX o la TIMEFORMATvariabile specifica 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*}

Se ti interessa solo il tempo dell'orologio a muro, correre dateprima e dopo è una soluzione semplice (se leggermente più imprecisa a causa del tempo extra impiegato per caricare il comando esterno).


Wow, molto da testare. Grazie mille per la risposta estesa. +1.
0xC0000022L

7

Con il tempo, l'output del comando esce su stdout e il tempo esce su stderr. Quindi, per separarli, puoi fare:

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

Ma ora è il momento in un file. Non penso che Bash sia in grado di inserire direttamente stderr in una variabile. Se non ti dispiace reindirizzare l'output del comando da qualche parte, puoi fare:

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

Quando lo fai, l'output del comando sarà attivo e sarà outputfileil tempo impiegato per l'esecuzione $FOO.


1
ooh, non me ne sono reso conto. Sapevo che timescrive a stderr, ma non mi rendevo conto che avrebbe unito stdout e stderr del comando eseguito nel suo stdout. Ma generalmente non esiste un metodo diretto (ovvero senza file intermedio)? +1.
0xC0000022L

3
@STATUS_ACCESS_DENIED: timenon unisce i suoi comandi stdoute stderr. Il modo in cui ho mostrato presupponeva che fosse necessario solo l'output stdout del comando. Dal momento bashche memorizzerà solo ciò che viene fuori stdout, devi reindirizzare. Nel caso in cui puoi tranquillamente rilasciare lo stderr del tuo comando: il tempo sarà sempre sull'ultima riga di stderr. Se hai bisogno di entrambi i flussi di output del tuo comando, suggerirei di inserirlo in un altro script.
marinus,

6

Se ci si trova bash(e non sh) e NON è necessaria una precisione inferiore al secondo, è possibile saltare completamente la chiamata dateed eseguirla senza generare ulteriori processi, senza dover separare l'output combinato e senza dover acquisire e analizzare l'output da qualsiasi comando:

# 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.

Bello. Non ho mai sentito parlare di
SECONDS

Ti capita di sapere a partire da quale versione di Bash è stata introdotta?
0xC0000022L

4

A tale scopo è probabilmente meglio usare times(rispetto time) in bashcui stampa i tempi accumulati dell'utente e del sistema per la shell e per i processi eseguiti dalla shell, ad esempio usare:

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

Vedi: help -m timesper maggiori informazioni.


4

Nel mettere insieme tutte le risposte precedenti, quando in OSX

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

puoi fare come

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"
}

1

Installa /bin/time(ad es. pacman -S time)

Quindi, invece di errore durante il tentativo di -fflag:

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

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

Puoi effettivamente usarlo:

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

E ottieni quello che vuoi - tempo in variabile (esempio utilizza %eper il tempo reale trascorso, per il controllo di altre opzioni man time):

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

echo "$variable"

/usr/bin/timesu molti sistemi
Basile Starynkevitch,

Se guardi attentamente, l'ho sottolineato nella mia domanda. timeè una shell incorporata in Bash (e presumibilmente altre shell) ma fintanto che è presente il percorso timedell'eseguibile PATH, possiamo usare command timeper assicurarci di eseguire il comando esterno anziché l'integrato.
0xC0000022L

1

Proprio come un avvertimento quando si lavora con le dichiarazioni di cui sopra e soprattutto tenendo presente la risposta di Grzegorz. Sono stato sorpreso di vedere sul mio sistema Ubuntu 16.04 Xenial questi due risultati:

$ 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

Non ho nessun alias impostato e quindi non so perché questo accada.


1
timeè anche una shell integrata.
Basile Starynkevitch,

Se vuoi assicurarti di eseguire il comando, usa command, se vuoi assicurarti di eseguire il comando incorporato, usa builtin. Non c'è magia qui. Utilizzare helpper capire i comandi disponibili o type <command>per capire di che tipo <command>è (ad esempio un comando incorporato, un comando esterno, una funzione o un alias).
0xC0000022L

Basile, hai ragione. Non avevo notato il significato del commandcomando. Ciò significa che viene chiamato / usr / bin / time. Ciò significa che le seguenti due affermazioni sono compatibili: $ command time -f %e sleep 4e$ /usr/bin/time -f %e sleep
Paul Pritchard,

0

prova questo, esegue un semplice comando con argomenti e inserisce i tempi $ real $ user $ sys e conserva il codice di uscita.

Inoltre, non esegue il fork delle subshells o calpesta le variabili tranne il vero sistema utente e non interferisce altrimenti con l'esecuzione dello script

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

per esempio

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

nota: per il comando semplice non è solo una pipeline, i cui componenti vengono eseguiti in una subshell

Questa versione consente di specificare come $ 1 il nome delle variabili che dovrebbero ricevere le 3 volte:

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
}

per esempio

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

e può essere utile se finisce per essere chiamato ricorsivamente, per evitare di calpestare i tempi; ma poi rus etc dovrebbe essere dichiarato locale nel loro uso.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.