Pieni nopeustesti huvin ja uteliaisuuden vuoksi

Moni tietää, että Linuxissa on paljon tekstivirtojen käsittelyyn liittyviä työkaluohjelmia. Eräs yleinen pikku ongelma on tulostaa tekstivirran kaikki rivit paitsi ensimmäinen, koska se ensimmäinen on jonkinlainen otsikkorivi kuten ps komennon tulosteessa:

[kalevi@localhost test]$ ps -ef | head
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 17:06 ? 00:00:01 /sbin/init
root 2 0 0 17:06 ? 00:00:00 [kthreadd]
root 3 2 0 17:06 ? 00:00:00 [ksoftirqd/0]
root 4 2 0 17:06 ? 00:00:00 [migration/0]
root 5 2 0 17:06 ? 00:00:00 [watchdog/0]
root 6 2 0 17:06 ? 00:00:00 [migration/1]
root 7 2 0 17:06 ? 00:00:00 [ksoftirqd/1]
root 8 2 0 17:06 ? 00:00:00 [watchdog/1]
root 9 2 0 17:06 ? 00:00:00 [migration/2]

Ongelman voi helposti ratkaista ainakin kolmella ohjelmalla. Laitetaan ensin ps -ef komennon tuloste tiedostoon, jotta ohjelmille annettu syöte olisi aina samanlainen:

[kalevi@localhost test]$ ps -ef > ps_out

Syötetiedosto ei ole suuri:

[kalevi@localhost test]$ wc -l ps_out
226 ps_out
[kalevi@localhost test]$ ls -l ps_out
-rw-rw-r--. 1 kalevi kalevi 26307 Dec 8 18:33 ps_out

bash komentotulkkia varten tehty skriptitiedosto on seuraavanlainen. Koska emme tässä tapauksessa ole kiinnostuneita näkemään komentojen tulostetta terminaalillamme vaan ainoastaan ohjelmien suoritusnopeuden, me ohjaamme niiden stdout virran /dev/null pseudotiedostoon, jolloin tuloste menee käyttöjärjestelmän toimesta suoraan "roskikseen":

[kalevi@localhost test]$ cat runtests
time for ((i=0; i<3000; i++)); do awk 'NR>1' ps_out > /dev/null; done
time for ((i=0; i<3000; i++)); do sed '1d' ps_out > /dev/null; done
time for ((i=0; i<3000; i++)); do tail -n +2 ps_out > /dev/null; done
time for ((i=0; i<3000; i++)); do ./skipfirst ps_out > /dev/null; done

Linuxissa valmiina olevat ohjelmat ovat awk, sed ja tail. skipfirst on oma pikainen kotikutoinen C-ohjelmamme, joka on mukana vain vertailun vuoksi. Tiedän kyllä hyvin sen virheet ja että se ei ole tuotantokelpoinen. Ohjelma sopii kuitenkin testikäyttöömme ja näyttää seuraavalta:

#include <stdio.h>
#include <stdlib.h>


#define MAXLINELEN 4096


int main(int argc, char *argv[])
{
        char linebuf[MAXLINELEN];
        char *infile;
        FILE *fp;

        if (argc != 2) {
                fprintf(stderr, "usage: skipfirst [infile]\n");
                exit(EXIT_FAILURE);
        }

        infile = argv[1];

        if ((fp = fopen(infile, "r")) == NULL) {
                fprintf(stderr, "could not open %s\n", infile);
                exit(EXIT_FAILURE);
        }


        (void)fgets(linebuf, sizeof(linebuf), fp);

        while (fgets(linebuf, sizeof(linebuf), fp)) {
                fputs(linebuf, stdout);
        }

        (void)fclose(fp);


        return 0;
}

Käänsin sen komennolla:

[kalevi@localhost test] gcc -O3 -W -Wall -o skipfirst skipfirst.c

Tehdään seuraavaksi kolme ajokertaa, joista jokainen suorittaa kunkin komennon 3000 kertaa:

[kalevi@localhost test]$ ./runtests

real 0m6.646s
user 0m0.786s
sys 0m3.781s

real 0m5.674s
user 0m0.455s
sys 0m3.347s

real 0m4.647s
user 0m0.536s
sys 0m3.280s

real 0m4.691s
user 0m0.697s
sys 0m3.209s
[kalevi@localhost test]$ ./runtests

real 0m6.505s
user 0m0.767s
sys 0m3.766s

real 0m5.754s
user 0m0.486s
sys 0m3.454s

real 0m4.682s
user 0m0.576s
sys 0m3.318s

real 0m4.631s
user 0m0.706s
sys 0m3.178s
[kalevi@localhost test]$ ./runtests

real 0m6.684s
user 0m0.836s
sys 0m3.811s

real 0m5.704s
user 0m0.529s
sys 0m3.327s

real 0m4.615s
user 0m0.552s
sys 0m3.252s

real 0m4.586s
user 0m0.649s
sys 0m3.100s

awk 'NR>1' skriptimme oli joka kerralla hitain noin 6,6 sekunnin suoritusajallaan. Sitä hieman parempi oli sed '1d' skripti, joka oli noin sekunnin nopeampi. Komento tail -n +2 oli joka kerralla noin sekunnin nopeampi kuin sed '1d' skripti. Oma C-ohjelmamme skipfirst oli käytännössä yhtä nopea kuin tail -n +2.

Onko tällä testillä mitään käytännön merkitystä? Eipä juuri, mutta jos joskus halutaan mikro-optimoida esimerkiksi jotain shelliskriptiä, joka käyttää jotain noista kolmesta ohjelmasta ensimmäisten rivien poisjättämiseen, niin nopeinta lienee käyttää tail komentoa. Se voi olla myös aavistuksen helpommin luettavampi vaihtoehto verrattuna kahteen muuhun skriptiin. Tarkistin myös POSIX-standardista, että -n +2 notaatio on käytettävissä standardinmukaisissa tail implementaatioissa. Eli kyseessä ei ole mikään Linux-spesifinen GNU-laajennos.