Sonntag, 28. Mai 2017

Analyse der Ausführungszeit eines Kommandos in der bash

Für diesen Anwendungsfall -- das Messen der Ausführungszeit eines Programms -- existiert das UNIX-Kommando time. Es ermittelt diese drei Werte:
  • real: die gesamte, tatsächliche Ausführungszeit (elapsed real time)
  • user: die Zeit, in der die CPU tatsächlich den (User-)Code des Programms ausführt (Total number of CPU-seconds that the process spent in user mode)
  • sys: jene Zeit, in der die CPU mit Ausführung von Kernel-Code für das Programm beschäftigt ist (Total number of CPU-seconds that the process spent in kernel mode) 

Beispiel 1

Um die Zeit zu ermitteln, rufen Sie time vor ihrem eigentlichen Kommando auf:

$ time find . -user frank
...
real   0m3.681s
user   0m0.248s
sys    0m0.916s
$

Das Beispiel ermittelt alle Dateien, die dem Benutzer frank gehören. time rechnet aus, dass find dafür 3.6s benötigt.

Skripting in der Praxis

Spannender wird die Zeitmessung, wenn mehrere Kommandos zusammenspielen, bspw. in einem Shellskript. Nachfolgend geht es darum, die User ID und den dazugehörigen Namen des UNIX-Nutzers zu ermitteln, der ein Skript gerade aufruft. Variante 1 greift auf interne Variablen und das Kommando whoami zurück:

#!/bin/bash
userId=$UID
userName=$(whoami)
echo "the script runs as user $userName with UNIX id $userId"

Variante 2 nutzt stattdessen grep und cut:

#!/bin/bash
userId=$UID 
userName=$(grep ":$UID:" /etc/passwd | cut -d: -f 1)
echo "the script runs as user $userName with UNIX id $userId"

In Variante 3 kommen grep und awk zum Einsatz:

#!/bin/bash
userId=$UID
userName=$(grep ":$UID:" /etc/passwd | awk -F : '{ print $1 }')
echo "the script runs as user $userName with UNIX id $userId"

Für Puristen ist die Variante 4 -- diese nutzt ausschließlich awk:

#!/bin/bash
userId=$UID
userName=$(awk -F : '/:'$userId':/ { print $1 }' /etc/passwd)
echo "the script runs as user $userName with UNIX id $userId"

Das das auch mit einer Skriptsprache wie Perl geht, zeigt das Variante 5:

#!/usr/bin/perl -w
$username=$ENV{'LOGNAME'};

# read /etc/passwd
open(FH, "/etc/passwd") || die "Cannot open /etc/passwd: $!";
{
  while(<FH>) {
    # look for the user name
    if (m/^${username}:/) {
      @fields=split(/:/, $_); 
 
      # extract user id
      $uid=$fields[2];
      print "the script runs as user $username with UNIX id $uid\n";
     }
   }
}
close(FH);

Auswertung der Laufzeit

Nun wird es spannend, welche der fünf Varianten am flinkesten ist. Rufen Sie die Konstrukte alle einzeln auf, liefert time bei allen einen Wert von etwa 1s. Um die Unterschiede deutlicher sichtbar werden zu lassen, bauen wir eine Schleife drumherum und lassen das ganze 10000 Mal durchlaufen:

#!/bin/bash
# define loops
loops=10000

time {
  for((i=$loops; i>0; i--))
  do
    userId=$UID
    userName=$(whoami)
    echo "the script runs as user $userName with UNIX id $userId"
  done
} | grep "real"

Analog erfolgt das auch für die Varianten 2 bis 5. Folgendes Ergebnis wird sichtbar:

Variante Ausführungszeit
Variante 1 mit whoami 8.25s
Variante 2 mit grep und cut 11.03s
Variante 3 mit grep und awk 17.1s
Variante 4 nur mit awk 13.9s
Variante 5 mit Perl 15.22s

Fazit

Eindeutiger Gewinner ist Variante 1, gefolgt Variante 2 und Variante 4. Es hilft also doch, bei der Programmierung stets die Laufzeit im Blick zu behalten.

Links

Keine Kommentare:

Kommentar veröffentlichen