Cómo medir el tiempo de ejecución del programa y almacenarlo dentro de una variable

61

Para saber cuánto tiempo tardan ciertas operaciones dentro de un script Bash (v4 +), me gustaría analizar el resultado del timecomando "por separado" y (en última instancia) capturarlo dentro de una variable Bash ( let VARNAME=...).

Ahora, estoy usando time -f '%e' ...(o más bien command time -f '%e' ...debido al Bash incorporado), pero dado que ya redirijo la salida del comando ejecutado, realmente estoy perdido en cuanto a cómo capturaría la salida del timecomando. Básicamente, el problema aquí es separar la salida de timela salida de los comandos ejecutados.

Lo que quiero es la funcionalidad de contar la cantidad de tiempo en segundos (enteros) entre el inicio de un comando y su finalización. No tiene que ser el timecomando o el respectivo incorporado.


Editar: dadas las dos respuestas útiles a continuación, quería agregar dos aclaraciones.

  1. No quiero tirar la salida del comando ejecutado, pero realmente no importará si termina en stdout o stderr.
  2. Preferiría un enfoque directo sobre uno indirecto (es decir, capturar la salida directamente en lugar de almacenarla en archivos intermedios).

La solución que utilizo datehasta ahora se cierra a lo que quiero.

0xC0000022L
fuente
La forma más directa de obtener los datos y manejarlos mientras se deja que se ejecute normalmente sería hacerlo en un programa en C usando fork(), execvp()y wait3()/wait4(). En última instancia, esto es lo que están haciendo el tiempo y los amigos. No conozco una manera simple de hacerlo en bash / perl sin redirigir a un archivo o enfoque similar.
penguin359
Hay una pregunta relacionada que puede encontrar interesante aquí .
Caleb
@Caleb: gracias. De hecho interesante. Sin embargo, para mis propósitos es suficiente hacerlo en un script.
0xC0000022L

Respuestas:

85

Para obtener la salida de timeen una var, use lo siguiente:

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

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

También puede solicitar un solo tipo de hora, por ejemplo, utime:

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

Para obtener el tiempo que también puede usar date +%s.%N, tómelo antes y después de la ejecución y calcule la diferencia:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
binfalse
fuente
44
Sin embargo, no quería tirar la salida del comando. Así que supongo que su tercer bloque de código es el más cercano a lo que tenía en mente. Aunque escribiría el último como DIFF=$((END-START)), haciendo uso de las expresiones aritméticas. :) ... gracias por la respuesta. +1
0xC0000022L
1
@STATUS_ACCESS_DENIED: Bash no hace aritmética de coma flotante, por lo que si desea una resolución mejor que la segunda ( .%Nen el código de binfalse), necesita bccálculos más sofisticados.
Gilles 'SO- deja de ser malvado'
@Gilles: Lo sé. Como escribí en mi pregunta, los enteros están bien. No es necesario tener una resolución superior a la segunda. Pero gracias, la fecha de invocación tendría que ser cambiada. Sin embargo, me di cuenta de eso.
0xC0000022L
66
Para su información, el formato de fecha% N no parece funcionar en Mac OS X, solo devuelve "N". Ok en Ubuntu.
David
Tenga en cuenta que si bien time (cmd) 2> somethingfunciona para redirigir la salida de temporización file, no está destinado a (según la documentación), no lo hace en otros shells donde timehay una palabra clave y podría considerarse como un error . No confiaría en él, ya que puede no funcionar en futuras versiones de bash.
Stéphane Chazelas
17

En bash, el resultado de la timeconstrucción va a su error estándar, y puede redirigir el error estándar de la tubería que afecta. Así que vamos a empezar con un comando que escribe a sus streamas de salida y de error: sh -c 'echo out; echo 1>&2 err'. Para no mezclar la secuencia de error del comando con la salida de time, podemos desviar temporalmente la secuencia de error del comando a un descriptor de archivo diferente:

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

Esto escribe outen fd 1, erren fd 3 y los tiempos en 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:/')

Sería más agradable tenerlo erren fd 2 y las veces en fd 3, por lo que los intercambiamos, lo cual es engorroso porque no hay una forma directa de intercambiar dos descriptores de archivo:

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

Esto muestra cómo puede postprocesar la salida del comando, pero si desea capturar tanto la salida del comando como sus tiempos, debe trabajar más duro. Usar un archivo temporal es una solución. De hecho, es la única solución confiable si necesita capturar tanto el error estándar del comando como su salida estándar. Pero de lo contrario, puede capturar todo el resultado y aprovechar el hecho de que timetiene un formato predecible (si lo usa time -ppara obtener el formato POSIX o la TIMEFORMATvariable específica de 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*}

Si solo le importa el tiempo del reloj de pared, la ejecución dateantes y después es una solución simple (si es un poco más imprecisa debido al tiempo extra dedicado a cargar el comando externo).

Gilles 'SO- deja de ser malvado'
fuente
Wow, mucho para probar. Muchas gracias por la extensa respuesta. +1.
0xC0000022L
7

Con el tiempo, la salida del comando sale en stdout y el tiempo sale en stderr. Entonces, para separarlos, puedes hacer:

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

Pero, ahora el tiempo está en un archivo. No creo que Bash pueda poner stderr en una variable directamente. Si no le importa redirigir la salida del comando a alguna parte, puede hacer lo siguiente:

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

Cuando haga esto, la salida del comando estará activada outputfiley el tiempo que tardó en ejecutarse $FOO.

marinus
fuente
1
ooh, no me di cuenta Conocía las timeescrituras de stderr, pero no me di cuenta de que fusionaría stdout y stderr del comando ejecutado en su stdout. Pero, en general, ¿no hay un método directo (es decir, sin archivo intermedio)? +1.
0xC0000022L
3
@STATUS_ACCESS_DENIED: timeno combina sus comandos stdouty stderr. La forma en que lo mostré suponía que solo necesitabas la salida estándar del comando. Como bashsolo almacenará lo que salga stdout, debe redirigir. En caso de que pueda soltar con seguridad el comando stderr: el tiempo siempre estará en la última línea de stderr. Si necesita las dos secuencias de salida de su comando, le sugiero que lo envuelva en otro script.
marinus
6

Si está bash(y no sh) y NO necesita una precisión de menos de un segundo, puede omitir la llamada por datecompleto y hacerlo sin generar ningún proceso adicional, sin tener que separar la salida combinada y sin tener que capturar y analizar la salida de cualquier 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.
sanpath
fuente
Agradable. Nunca he oído hablar de SEGUNDOS antes
Bruno9779
¿Sabes a partir de qué versión de Bash se introdujo esto?
0xC0000022L
4

Para ese propósito, probablemente sea mejor usar times(que time) en el bashque imprime el usuario acumulado y los tiempos del sistema para el shell y para los procesos ejecutados desde el shell, por ejemplo use:

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

Ver: help -m timespara más información.

kenorb
fuente
4

Al poner todas las respuestas anteriores juntas, cuando en OSX

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

puedes hacer como

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
fuente
1

Instalar /bin/time(por ejemplo pacman -S time)

Entonces, en lugar de error al intentar -fmarcar:

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

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

Realmente puedes usarlo:

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

Y obtenga lo que desea: tiempo en variable (ejemplo, usos %epara el tiempo transcurrido real, para otras opciones de verificación man time):

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

echo "$variable"
Grzegorz Wierzowiecki
fuente
/usr/bin/timeen muchos sistemas
Basile Starynkevitch
Si te fijas bien, lo señalé en mi pregunta. timees un shell integrado en Bash (y presumiblemente otros shells), pero siempre que timeesté la ruta al ejecutable PATH, podemos usarlo command timepara asegurarnos de que estamos ejecutando el comando externo en lugar del incorporado.
0xC0000022L
1

Solo como un aviso al trabajar con las declaraciones anteriores y especialmente teniendo en cuenta la respuesta de Grzegorz. Me sorprendió ver en mi sistema Ubuntu 16.04 Xenial estos dos resultados:

$ 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

No tengo ningún alias establecido, por lo que no sé por qué sucede esto.

Paul Pritchard
fuente
1
timeTambién es una cáscara incorporada.
Basile Starynkevitch
Si desea asegurarse de que está ejecutando el comando, use command, si desea asegurarse de que está ejecutando el comando incorporado, use builtin. No hay magia aquí. Utilícelo helppara descubrir los comandos disponibles o type <command>para determinar qué tipo <command>es (por ejemplo, un comando incorporado, un comando externo, una función o un alias).
0xC0000022L
Basile, tienes toda la razón. No había visto la importancia del commandcomando. Lo cual es un hecho asegura que se llame a / usr / bin / time. Esto significa que las siguientes dos declaraciones son compatibles: $ command time -f %e sleep 4y$ /usr/bin/time -f %e sleep
Paul Pritchard
0

intente esto, ejecuta un comando simple con argumentos y pone los tiempos $ real $ user $ sys y conserva el código de salida.

Tampoco bifurca subcapas ni pisotea ninguna variable, excepto el sistema de usuario real, y no interfiere con la ejecución del 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
}

p.ej

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

nota: solo mide el tiempo del comando simple, no una tubería, cuyos componentes se ejecutan en una subshell

Esta versión le permite especificar como $ 1 el nombre de las variables que deberían recibir las 3 veces:

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
}

p.ej

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

y puede ser útil si termina siendo llamado de forma recursiva, para evitar pisotear a veces; pero luego rus, etc., deben declararse locales en su uso.

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

Sam Liddicott
fuente