Ho fatto il seguente test e sul mio sistema la differenza risultante è circa 100 volte più lunga per il secondo script.
Il mio file è un output di strace chiamato bigfile
$ wc -l bigfile.log
1617000 bigfile.log
Script
xtian@clafujiu:~/tmp$ cat p1.sh
tail -n 1000000 bigfile.log | grep '"success": true' | wc -l
tail -n 1000000 bigfile.log | grep '"success": false' | wc -l
xtian@clafujiu:~/tmp$ cat p2.sh
log=$(tail -n 1000000 bigfile.log)
echo "$log" | grep '"success": true' | wc -l
echo "$log" | grep '"success": true' | wc -l
In realtà non ho alcuna corrispondenza per il grep, quindi nulla viene scritto nell'ultima pipe fino a wc -l
Ecco i tempi:
xtian@clafujiu:~/tmp$ time bash p1.sh
0
0
real 0m0.381s
user 0m0.248s
sys 0m0.280s
xtian@clafujiu:~/tmp$ time bash p2.sh
0
0
real 0m46.060s
user 0m43.903s
sys 0m2.176s
Quindi ho eseguito nuovamente i due script tramite il comando strace
strace -cfo p1.strace bash p1.sh
strace -cfo p2.strace bash p2.sh
Ecco i risultati delle tracce:
$ cat p1.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
97.24 0.508109 63514 8 2 waitpid
1.61 0.008388 0 84569 read
1.08 0.005659 0 42448 write
0.06 0.000328 0 21233 _llseek
0.00 0.000024 0 204 146 stat64
0.00 0.000017 0 137 fstat64
0.00 0.000000 0 283 149 open
0.00 0.000000 0 180 8 close
...
0.00 0.000000 0 162 mmap2
0.00 0.000000 0 29 getuid32
0.00 0.000000 0 29 getgid32
0.00 0.000000 0 29 geteuid32
0.00 0.000000 0 29 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 7 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 0.522525 149618 332 total
E p2.strace
$ cat p2.strace
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
75.27 1.336886 133689 10 3 waitpid
13.36 0.237266 11 21231 write
4.65 0.082527 1115 74 brk
2.48 0.044000 7333 6 execve
2.31 0.040998 5857 7 clone
1.91 0.033965 0 705681 read
0.02 0.000376 0 10619 _llseek
0.00 0.000000 0 248 132 open
...
0.00 0.000000 0 141 mmap2
0.00 0.000000 0 176 126 stat64
0.00 0.000000 0 118 fstat64
0.00 0.000000 0 25 getuid32
0.00 0.000000 0 25 getgid32
0.00 0.000000 0 25 geteuid32
0.00 0.000000 0 25 getegid32
0.00 0.000000 0 3 1 fcntl64
0.00 0.000000 0 6 set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00 1.776018 738827 293 total
Analisi
Non sorprende che, in entrambi i casi, la maggior parte del tempo passi in attesa del completamento di un processo, ma p2 attende 2,63 volte più a lungo di p1 e, come altri hanno già detto, si inizia in ritardo in p2.sh.
Quindi ora dimentica il waitpid
, ignora la %
colonna e guarda la colonna dei secondi su entrambe le tracce.
Il tempo più grande p1 trascorre la maggior parte del suo tempo in lettura probabilmente comprensibile, perché c'è un file di grandi dimensioni da leggere, ma p2 impiega 28,82 volte in più in lettura rispetto a p1. - bash
non si aspetta di leggere un file così grande in una variabile e probabilmente legge il buffer alla volta, si divide in righe e ne ottiene un altro.
il numero di letture p2 è 705k contro 84k per p1, ciascuna lettura richiede un cambio di contesto nello spazio del kernel ed esce di nuovo. Quasi 10 volte il numero di letture e cambi di contesto.
Il tempo in scrittura p2 trascorre 41,93 volte più a lungo in scrittura di p1
il conteggio delle scritture p1 fa più scritture di p2, 42k contro 21k, tuttavia sono molto più veloci.
Probabilmente a causa delle echo
righe in grep
rispetto ai buffer di scrittura della coda.
Inoltre , p2 trascorre più tempo in scrittura che in lettura, p1 è il contrario!
Altro fattore Guarda il numero di brk
chiamate di sistema: p2 spende 2.42 volte più a lungo di quanto non legga! In p1 (non si registra nemmeno). brk
è quando il programma deve espandere il suo spazio di indirizzi perché inizialmente non è stato allocato abbastanza, ciò è probabilmente dovuto al fatto che bash deve leggere quel file nella variabile e non aspettarsi che sia così grande, e come ha detto @scai, se il il file diventa troppo grande, anche quello non funzionerebbe.
tail
è probabilmente un lettore di file abbastanza efficiente, poiché è quello che è stato progettato per fare, probabilmente memorizza il file e scansiona le interruzioni di linea, permettendo così al kernel di ottimizzare l'I / O. bash non è altrettanto buono sia nel tempo trascorso a leggere che a scrivere.
p2 impiega 44ms e 41ms in clone
e execv
non è un valore misurabile per p1. Probabilmente bash leggendo e creando la variabile dalla coda.
Finalmente il totale p1 esegue ~ 150k chiamate di sistema contro p2 740k (4.93 volte maggiore).
Eliminando waitpid, p1 impiega 0,014416 secondi per eseguire chiamate di sistema, p2 0,439132 secondi (30 volte più a lungo).
Quindi sembra che p2 passi la maggior parte del tempo nello spazio utente senza fare altro che aspettare che le chiamate di sistema vengano completate e che il kernel riorganizzi la memoria, p1 esegue più scritture, ma è più efficiente e causa un carico di sistema significativamente inferiore, quindi è più veloce.
Conclusione
Non proverei mai a preoccuparmi di scrivere codice attraverso la memoria quando scrivo uno script bash, ciò non significa che non cerchi di essere efficiente.
tail
è progettato per fare ciò che fa, probabilmente memory maps
il file in modo che sia efficiente da leggere e consente al kernel di ottimizzare l'I / O.
Un modo migliore per ottimizzare il tuo problema potrebbe essere innanzitutto quello grep
di "successo": "linee e poi contare le verità e i falsi, grep
ha un'opzione di conteggio che evita ancora wc -l
, o ancora meglio, awk
convogliare la coda e contare le verità e falsi contemporaneamente. p2 non solo richiede molto tempo, ma aggiunge carico al sistema mentre la memoria viene mescolata con i brks.