Comment mesurer le temps d'exécution du programme et le stocker dans une variable

61

Afin de savoir combien de temps prennent certaines opérations dans un script Bash (v4 +), j'aimerais analyser le résultat de la timecommande "séparément" et (au final) le capturer dans une variable Bash ( let VARNAME=...).

Maintenant, j'utilise time -f '%e' ...(ou plutôt à command time -f '%e' ...cause de Bash intégré), mais comme je redirige déjà la sortie de la commande exécutée, je suis vraiment perdu quant à la manière dont je voudrais capturer la sortie de la timecommande. En gros, le problème est de séparer la sortie de timela sortie de la ou des commandes exécutées.

Ce que je veux, c'est la fonctionnalité qui consiste à compter le temps en secondes (entiers) entre le lancement d'une commande et son achèvement. Il n'est pas nécessaire que ce soit la timecommande ou l'intégré respectif.


Edit: étant donné les deux réponses utiles ci-dessous, je voulais ajouter deux clarifications.

  1. Je ne veux pas jeter la sortie de la commande exécutée, mais peu importe si elle se termine sur stdout ou stderr.
  2. Je préférerais une approche directe à une approche indirecte (c.-à-d. Capturer la sortie directement plutôt que de la stocker dans des fichiers intermédiaires).

La solution en utilisant datejusqu'ici vient fermer à ce que je veux.

0xC0000022L
la source
Le moyen le plus direct d’obtenir les données et de les traiter tout en les laissant fonctionner normalement serait de le faire dans un programme C en utilisant fork(), execvp()et wait3()/wait4(). C'est finalement ce que font le temps et les amis. Je ne suis pas au courant d'une manière simple de le faire dans bash / perl sans rediriger vers un fichier ou une approche similaire.
penguin359
Il y a une question connexe que vous pourriez trouver intéressante se passe ici .
Caleb
@Caleb: merci. En effet intéressant. Cependant, pour mes besoins, il suffit de le faire dans un script.
0xC0000022L

Réponses:

85

Pour obtenir le résultat de timedans une variable, utilisez ce qui suit:

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

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

Vous pouvez également simplement demander un type d'heure unique, par exemple utime:

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

Pour obtenir le temps que vous pouvez également utiliser date +%s.%N, prenez-le donc avant et après l'exécution et calculez le diff:

START=$(date +%s.%N)
command
END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc)
# echo $DIFF
binfale
la source
4
Je ne voulais cependant pas jeter le résultat de la commande. Donc, je suppose que votre troisième bloc de code est le plus proche de ce que j'avais en tête. Bien que j'écrive le dernier comme DIFF=$((END-START)), utilisant les expressions arithmétiques. :) ... Merci d'avoir répondu. +1
0xC0000022L
1
@STATUS_ACCESS_DENIED: Bash ne fait pas d'arithmétique en virgule flottante, donc si vous voulez une résolution supérieure à la seconde résolution ( .%Ndans le code de binfalse), vous avez besoin de bccalculs plus élaborés.
Gilles 'SO- arrête d'être méchant'
@ Gilles: Je sais. Comme je l'ai écrit dans ma question, les entiers vont bien. Pas besoin d'avoir une résolution supérieure à la seconde. Mais merci, l'invocation de la date devra être changée. Je l'avais compris, cependant.
0xC0000022L
6
Pour votre information, le formatage de la date% N ne semble pas fonctionner sous Mac OS X, il renvoie simplement "N". Ok sur Ubuntu.
David
Notez que bien que la time (cmd) 2> somethingredirection vers la sortie de chronométrage soit efficace file, elle n’est pas censée (conformément à la documentation), pas dans les autres shells où timeun mot clé est considéré et qui pourrait être considéré comme un bogue . Je ne m'appuierais pas dessus car cela pourrait ne pas fonctionner dans les futures versions de bash.
Stéphane Chazelas
17

Sous bash, la sortie de la timeconstruction va à son erreur standard et vous pouvez rediriger l'erreur standard du pipeline qu'elle affecte. Alors commençons par une commande qui écrit à sa sortie et erreur streamas: sh -c 'echo out; echo 1>&2 err'. Afin de ne pas confondre le flux d'erreur de la commande avec le résultat time, nous pouvons temporairement renvoyer le flux d'erreur de la commande vers un autre descripteur de fichier:

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

Cela écrit outà jour 1, errà jour 3 et les temps à jour 2:

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

Il serait plus agréable d’avoir errle deuxième et l’heure du troisième, nous les échangeons, ce qui est fastidieux car il n’ya pas de moyen direct d’échanger deux descripteurs de fichier:

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

Cela montre comment vous pouvez post-traiter le résultat de la commande, mais si vous souhaitez capturer à la fois le résultat de la commande et son heure, vous devez travailler plus fort. L'utilisation d'un fichier temporaire est une solution. En fait, c'est la seule solution fiable si vous devez capturer à la fois l'erreur standard de la commande et sa sortie standard. Mais sinon, vous pouvez capturer toute la sortie et tirer parti du fait que timeson format est prévisible (si vous utilisez time -ppour obtenir le format POSIX ou la TIMEFORMATvariable spécifique à 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 vous ne vous souciez que de l'heure de l'horloge murale, exécuter dateavant et après est une solution simple (si elle est légèrement plus imprécise en raison du temps supplémentaire passé à charger la commande externe).

Gilles, arrête de faire le mal
la source
Wow, beaucoup à tester. Merci beaucoup pour la réponse détaillée. +1
0xC0000022L
7

Avec le temps, la sortie de la commande sort sur stdout et le temps sur stderr. Donc, pour les séparer, vous pouvez faire:

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

Mais, maintenant le temps est dans un fichier. Je ne pense pas que Bash puisse mettre stderr dans une variable directement. Si cela ne vous dérange pas de rediriger la sortie de la commande quelque part, vous pouvez faire:

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

Lorsque vous effectuez cette opération, le résultat de la commande sera outputfileentré et le temps qu’il aura fallu pour s’exécuter sera également $FOO.

marinus
la source
1
ooh, je n'avais pas réalisé. Je savais que timestderr était écrit, mais je ne savais pas que cela fusionnerait stdout et stderr de la commande exécutée dans sa stdout. Mais généralement, il n’existe pas de méthode directe (sans fichier intermédiaire)? +1
0xC0000022L
3
@STATUS_ACCESS_DENIED: timene fusionne pas les commandes stdoutet stderr. La façon dont j'ai montré suppose que vous n'aviez besoin que de la sortie stdout de la commande. Comme vous bashne stockez que ce qui sort stdout, vous devez rediriger. Dans le cas où vous pouvez supprimer en toute sécurité le stderr de votre commande: l'heure sera toujours sur la dernière ligne de stderr. Si vous avez besoin des deux flux de sortie de votre commande, je vous suggère de l'envelopper dans un autre script.
marinus
6

Si vous êtes bash(et pas sh) et que vous n'avez PAS besoin d'une précision inférieure à la seconde, vous pouvez ignorer l'appel dateet le faire entièrement sans générer de processus supplémentaires, sans avoir à séparer la sortie combinée, ni à capturer et à analyser la sortie. à partir de toutes commandes:

# 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
la source
Agréable. Je n'ai jamais entendu parler de SECONDES
Bruno9779 Le
Savez-vous à partir de quelle version de Bash a été introduit?
0xC0000022L
4

A cette fin , il est probablement préférable d'utiliser times(que time) dans bashlequel imprime les durées cumulées utilisateur et système pour le shell et les processus lancés à partir du shell, utilisez exemple:

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

Voir: help -m timespour plus d'informations.

Kenorb
la source
4

Dans la synthèse de toutes les réponses précédentes, lorsque sous OSX

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

tu peux faire comme

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
la source
1

Installer /bin/time(par exemple pacman -S time)

Donc au lieu d'erreur en essayant -fflag:

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

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

Vous pouvez réellement l'utiliser:

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

Et obtenez ce que vous voulez - temps en variable (exemple d'utilisation %epour le temps écoulé réel, pour vérifier les autres options man time):

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

echo "$variable"
Grzegorz Wierzowiecki
la source
/usr/bin/timesur de nombreux systèmes
Basile Starynkevitch le
Si vous regardez de plus près, je l’ai souligné dans ma question. timeest un shell intégré à Bash (et probablement à d’autres shell), mais tant que le chemin de l’ timeexécutable est activé PATH, nous pouvons command timenous assurer que nous exécutons la commande external et non l’intégré.
0xC0000022L
1

Juste comme un avertissement lorsque vous travaillez avec les déclarations ci-dessus et en prenant particulièrement en compte la réponse de Grzegorz. J'ai été surpris de voir sur mon système Ubuntu 16.04 Xenial ces deux résultats:

$ 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

Je n'ai pas de pseudonymes définis et je ne sais donc pas pourquoi cela se produit.

Paul Pritchard
la source
1
timeest aussi un shell intégré.
Basile Starynkevitch le
Si vous voulez vous assurer que vous exécutez la commande, utilisez command, si vous voulez vous assurer que vous exécutez la commande intégrée, utilisez builtin. Il n'y a pas de magie ici. Permet helpde déterminer les commandes disponibles ou type <command>de déterminer le type <command>(par exemple, une commande interne, une commande externe, une fonction ou un alias).
0xC0000022L
Basile, tu as bien raison. Je n'avais pas vu la signification de la commandcommande. C’est ce qui garantit que / usr / bin / time est appelé. Cela signifie que les deux déclarations suivantes sont compatibles: $ command time -f %e sleep 4et$ /usr/bin/time -f %e sleep
Paul Pritchard le
0

essayez ceci, il exécute une commande simple avec des arguments et met les temps $ real $ user $ sys et préserve le code de sortie.

De plus, il ne crée pas de sous-shell et ne piétine aucune variable, à l'exception du système utilisateur réel, et n'interfère pas avec l'exécution du 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
}

par exemple

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

remarque: il ne fait que multiplier la commande simple par un pipeline, dont les composants sont exécutés dans un sous-shell

Cette version vous permet de spécifier comme $ 1 le nom des variables devant recevoir les 3 fois:

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
}

par exemple

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

et peut être utile si elle est appelée récursivement pour éviter de piétiner les temps; mais alors rus etc. devraient être déclarés locaux dans leur utilisation.

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

Sam Liddicott
la source