Perché l'apertura di un file è più veloce della lettura di contenuti variabili?


36

In uno bashscript ho bisogno di vari valori dai /proc/file. Fino ad ora ho dozzine di righe che inseriscono i file in questo modo:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

Nel tentativo di renderlo più efficiente ho salvato il contenuto del file in una variabile e grepped che:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'

Invece di aprire il file più volte questo dovrebbe semplicemente aprirlo una volta e grep il contenuto della variabile, che ho assunto sarebbe più veloce - ma in realtà è più lento:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s

Lo stesso vale per dashe zsh. Ho sospettato lo stato speciale dei /proc/file come motivo, ma quando copio il contenuto /proc/meminfoin un file normale e uso che i risultati sono gli stessi:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

L'uso di una stringa qui per salvare la pipe lo rende leggermente più veloce, ma non così veloce come con i file:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

Perché l'apertura di un file è più veloce della lettura dello stesso contenuto da una variabile?


@ l0b0 Questo presupposto non è errato, la domanda mostra come sono arrivato a farlo e le risposte spiegano perché questo è il caso. La tua modifica ora fa sì che le risposte non rispondano più alla domanda del titolo: non dicono se è così.
dessert

OK, chiarito. Poiché l'intestazione era errata nella stragrande maggioranza dei casi, non solo per alcuni file speciali mappati in memoria.
l0b0

@ l0b0 No, questo è quello che sto chiedendo qui: “Sospettavo lo stato speciale di /proc/file come una ragione, ma quando copio il contenuto di /proc/meminfoun file regolare e l'uso che i risultati sono gli stessi:” E ' non è speciale /proc/anche la lettura di file regolari è più veloce!
dessert

Risposte:


47

Qui, non si tratta di aprire un file contro la lettura di contenuti di una variabile , ma più su fork di un processo più o meno.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfoesegue un fork che esegue un processo che si grepapre /proc/meminfo(un file virtuale, in memoria, nessun I / O del disco coinvolto) lo legge e corrisponde al regexp.

La parte più costosa in ciò è il fork del processo e il caricamento dell'utilità grep e delle sue dipendenze della libreria, il collegamento dinamico, l'apertura del database delle impostazioni locali, dozzine di file che sono sul disco (ma probabilmente memorizzati nella cache).

La parte relativa alla lettura /proc/meminfoè insignificante in confronto, il kernel ha bisogno di poco tempo per generare le informazioni lì dentro e ha grepbisogno di poco tempo per leggerle.

Se lo esegui strace -c, vedrai che le chiamate di sistema uno open()e uno read()utilizzate per leggere /proc/meminfosono noccioline rispetto a tutto ciò che grepfa per iniziare ( strace -cnon conta il fork).

Nel:

a=$(</proc/meminfo)

Nella maggior parte delle shell che supportano $(<...)quell'operatore ksh, la shell apre semplicemente il file e legge il suo contenuto (e rimuove i caratteri di nuova riga finali). bashè diverso e molto meno efficiente in quanto prevede un processo per eseguire tale lettura e passa i dati al genitore tramite una pipe. Ma qui, è fatto una volta, quindi non importa.

Nel:

printf '%s\n' "$a" | grep '^MemFree'

La shell deve generare due processi, che sono in esecuzione contemporaneamente ma interagiscono tra loro tramite una pipe. Quella creazione di pipe, lo smantellamento, la scrittura e la lettura da essa ha un costo limitato. Il costo molto maggiore è la generazione di un processo aggiuntivo. Anche la pianificazione dei processi ha un certo impatto.

Potresti scoprire che l'uso <<<dell'operatore zsh lo rende leggermente più veloce:

grep '^MemFree' <<< "$a"

In zsh e bash, ciò avviene scrivendo il contenuto di $aun file temporaneo, che è meno costoso rispetto alla generazione di un processo aggiuntivo, ma probabilmente non ti darà alcun guadagno rispetto al recupero immediato dei dati /proc/meminfo. È ancora meno efficiente del tuo approccio che copia /proc/meminfosu disco, poiché la scrittura del file temporaneo viene eseguita ad ogni iterazione.

dashnon supporta le stringhe qui, ma i suoi heredoc sono implementati con una pipe che non comporta la generazione di un processo aggiuntivo. Nel:

 grep '^MemFree' << EOF
 $a
 EOF

La shell crea una pipe, crea un processo. Il bambino esegue grepcon il suo stdin come estremità di lettura della pipe e il genitore scrive il contenuto all'altra estremità della pipe.

Ma è probabile che la gestione dei tubi e la sincronizzazione dei processi siano ancora più costosi rispetto al semplice recupero dei dati /proc/meminfo.

Il contenuto di /proc/meminfoè breve e non richiede molto tempo per la produzione. Se si desidera salvare alcuni cicli della CPU, si desidera rimuovere le parti costose: processi di fork e comandi esterni.

Piace:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

Evita bashperò che il pattern matching sia molto inefficace. Con zsh -o extendedglob, puoi accorciarlo a:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}

Nota che ^è speciale in molte conchiglie (Bourne, fish, rc, es e zsh con almeno l'opzione extendedglob), ti consiglio di citarlo. Si noti inoltre che echonon può essere utilizzato per generare dati arbitrari (da qui il mio uso di cui printfsopra).


4
Nel caso in cui printftu dica che la shell deve generare due processi, ma non è printfuna shell integrata?
David Conrad,

6
@DavidConrad Lo è, ma la maggior parte delle shell non tenta di analizzare la pipeline per quali parti potrebbe essere eseguita nel processo corrente. Si forgia e lascia che i bambini lo capiscano. In questo caso, il processo genitore esegue due fork; il bambino per il lato sinistro vede quindi un built-in e lo esegue; il bambino per il lato destro vede greped esegue.
Chepner,

1
@DavidConrad, la pipe è un meccanismo IPC, quindi in ogni caso le due parti dovranno correre in processi diversi. Mentre A | Bci sono, ci sono alcune shell come AT&T ksh o zsh che sono Bin esecuzione nel processo di shell corrente se si tratta di un comando incorporato o composto o di funzione, non conosco nessuna di quelle che vengono eseguite Anel processo corrente. Semmai, per farlo, dovrebbero gestire SIGPIPE in modo complesso come se Afosse in esecuzione in un processo figlio e senza terminare la shell affinché il comportamento non sia troppo sorprendente quando Besce presto. È molto più semplice eseguirlo Bnel processo padre.
Stéphane Chazelas,

Bash supporta<<<
D. Ben Knoble il

1
@ D.BenKnoble, non intendevo implicare bashche non supportasse <<<, solo che l'operatore veniva da zshcome $(<...)veniva da ksh.
Stéphane Chazelas,

6

Nel tuo primo caso stai solo usando l'utilità grep e trovi qualcosa dal file /proc/meminfo, /procè un file system virtuale quindi il /proc/meminfofile è in memoria e richiede pochissimo tempo per recuperarne il contenuto.

Ma nel secondo caso, si sta creando una pipe, quindi passando l'output del primo comando al secondo comando utilizzando questa pipe, che è costoso.

La differenza è dovuta a /proc(perché è in memoria) e pipe, vedi l'esempio seguente:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s

1

Stai chiamando un comando esterno in entrambi i casi (grep). La chiamata esterna richiede una subshell. Il fork di quel guscio è la causa fondamentale del ritardo. Entrambi i casi sono simili, quindi: un ritardo simile.

Se vuoi leggere il file esterno solo una volta e usarlo (da una variabile) più volte, non esci dalla shell:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

Il che richiede solo circa 0,1 secondi invece dell'intero 1 secondo per la chiamata grep.

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.