Perché c'è una tale differenza nei tempi di esecuzione di eco e gatto?


15

Rispondere a questa domanda mi ha portato a porre un'altra domanda:
ho pensato che i seguenti script facessero la stessa cosa e il secondo dovrebbe essere molto più veloce, perché il primo usa catche deve aprire il file più e più volte ma il secondo apre solo il file una volta e poi fa eco solo a una variabile:

(Vedi la sezione aggiornamento per il codice corretto.)

Primo:

#!/bin/sh
for j in seq 10; do
  cat input
done >> output

Secondo:

#!/bin/sh
i=`cat input`
for j in seq 10; do
  echo $i
done >> output

mentre l'input è di circa 50 megabyte.

Ma quando ho provato il secondo, è stato troppo lento perché l'eco della variabile è istato un processo enorme. Ho anche avuto alcuni problemi con il secondo script, ad esempio la dimensione del file di output era inferiore al previsto.

Ho anche controllato la pagina man di echoe catper confrontarli:

echo - mostra una riga di testo

cat - concatena i file e stampa sull'output standard

Ma non ho capito la differenza.

Così:

  • Perché cat è così veloce e l'eco è così lento nella seconda sceneggiatura?
  • O il problema con la variabile i? (perché nella pagina man echosi dice che mostra "una riga di testo" e quindi immagino che sia ottimizzato solo per variabili brevi, non per variabili molto molto lunghe come i. Tuttavia, questa è solo un'ipotesi.)
  • E perché ho problemi quando uso echo?

AGGIORNARE

Ho usato seq 10invece che in `seq 10`modo errato. Questo è un codice modificato:

Primo:

#!/bin/sh
for j in `seq 10`; do
  cat input
done >> output

Secondo:

#!/bin/sh
i=`cat input`
for j in `seq 10`; do
  echo $i
done >> output

(Un ringraziamento speciale a roaima .)

Tuttavia, non è il punto del problema. Anche se il ciclo si verifica solo una volta, ottengo lo stesso problema: catfunziona molto più velocemente di echo.


1
e che dire cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk,

2
Il echoè più veloce. Quello che ti manca è che stai facendo funzionare troppo la shell senza citare le variabili quando le usi.
roaima,

Citando le variabili non è il problema; il problema è la variabile i stessa (cioè usandola come un passaggio intermedio tra input e output).
Aleksander,

`echo $ i` - non farlo. Usa printf e cita l'argomento.
PSkocik,

1
@PSkocik Quello che sto dicendo è che printf '%s' "$i"non lo vuoi echo $i. @cuonglm spiega alcuni dei problemi dell'eco nella sua risposta. Perché anche la citazione non è sufficiente in alcuni casi con l'eco, vedere unix.stackexchange.com/questions/65803/…
PSkocik,

Risposte:


24

Ci sono molte cose da considerare qui.

i=`cat input`

può essere costoso e ci sono molte variazioni tra le shell.

Questa è una funzione chiamata sostituzione dei comandi. L'idea è di memorizzare l'intero output del comando meno i caratteri newline finali nella ivariabile in memoria.

Per fare ciò, le shell fork il comando in una subshell e leggono il suo output attraverso una pipe o un socketpair. Vedi molte variazioni qui. Su un file di 50 MiB qui, posso vedere per esempio che bash è 6 volte più lento di ksh93 ma leggermente più veloce di zsh e due volte più veloce di yash.

Il motivo principale per cui bashè lento è che legge dalla pipe 128 byte alla volta (mentre altre shell leggono 4KiB o 8 KiB alla volta) ed è penalizzata dall'overhead della chiamata di sistema.

zshha bisogno di fare un po 'di post-elaborazione per sfuggire ai byte NUL (altre shell si rompono sui byte NUL) e yashfa un'elaborazione ancora più pesante analizzando i caratteri multi-byte.

Tutte le conchiglie devono eliminare i caratteri di fine riga finali che potrebbero eseguire in modo più o meno efficiente.

Alcuni potrebbero voler gestire i byte NUL in modo più elegante rispetto ad altri e verificarne la presenza.

Quindi una volta che hai quella grande variabile in memoria, qualsiasi manipolazione su di essa comporta generalmente l'allocazione di più memoria e la gestione dei dati.

Qui, stai passando (intendendo passare) il contenuto della variabile a echo.

Fortunatamente, echoè integrato nella tua shell, altrimenti l'esecuzione sarebbe probabilmente fallita con un errore troppo lungo nella lista arg . Anche in questo caso, la creazione dell'array dell'elenco di argomenti comporterà probabilmente la copia del contenuto della variabile.

L'altro problema principale nel tuo approccio di sostituzione dei comandi è che stai invocando l' operatore split + glob (dimenticando di citare la variabile).

Per questo, le shell devono trattare la stringa come una stringa di caratteri (anche se alcune shell non lo fanno e sono buggy a tale riguardo), quindi nelle localizzazioni UTF-8, ciò significa analizzare le sequenze UTF-8 (se non già fatto come yashfa) , cerca i $IFScaratteri nella stringa. Se $IFScontiene spazio, tab o newline (che è il caso per impostazione predefinita), l'algoritmo è ancora più complesso e costoso. Quindi, le parole risultanti da tale scissione devono essere allocate e copiate.

La parte globale sarà ancora più costosa. Se uno qualsiasi di queste parole contengono caratteri glob ( *, ?, [), allora la shell dovrà leggere il contenuto di alcune directory e fare un po 'il pattern matching costoso ( bash's implementazione per esempio è notoriamente molto male a quello).

Se l'input contiene qualcosa del genere /*/*/*/../../../*/*/*/../../../*/*/*, sarà estremamente costoso in quanto ciò significa elencare migliaia di directory e che può espandersi a diverse centinaia di MiB.

Quindi echoeseguirà in genere qualche elaborazione aggiuntiva. Alcune implementazioni espandono le \xsequenze nell'argomento che riceve, il che significa analizzare il contenuto e probabilmente un'altra allocazione e copia dei dati.

D'altra parte, OK, nella maggior parte delle shell catnon è integrato, quindi ciò significa biforcare un processo ed eseguirlo (quindi caricare il codice e le librerie), ma dopo la prima chiamata, quel codice e il contenuto del file di input verrà memorizzato nella cache. D'altra parte, non ci sarà alcun intermediario. catleggerà grandi quantità alla volta e la scriverà immediatamente senza elaborarla, e non è necessario allocare grandi quantità di memoria, solo quell'unico buffer che riutilizza.

Significa anche che è molto più affidabile in quanto non soffoca sui byte NUL e non taglia i caratteri di nuova riga finali (e non fa dividere + glob, anche se puoi evitarlo citando la variabile e non espandere la sequenza di escape sebbene sia possibile evitarlo utilizzando printfinvece di echo).

Se vuoi ottimizzarlo ulteriormente, invece di invocare catpiù volte, passa inputpiù volte a cat.

yes input | head -n 100 | xargs cat

Eseguirà 3 comandi anziché 100.

Per rendere la versione variabile più affidabile, dovresti usare zsh(altre shell non possono far fronte ai byte NUL) e farlo:

zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"

Se sai che l'input non contiene byte NUL, puoi farlo in modo affidabile POSIX (anche se potrebbe non funzionare dove printfnon è incorporato) con:

i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
  printf %s "$i"
  n=$((n - 1))
done

Ma questo non sarà mai più efficiente dell'uso catnel loop (a meno che l'input non sia molto piccolo).


Vale la pena ricordare che in caso di lunghe discussioni, è possibile ottenere la memoria . Esempio/bin/echo $(perl -e 'print "A"x999999')
cuonglm,

Ti sbagli sul presupposto che la dimensione della lettura abbia un'influenza significativa, quindi leggi la mia risposta per capire il vero motivo.
schily,

@schily, fare 409600 letture di 128 byte richiede più tempo (tempo di sistema) di 800 letture di 64k. Confronta dd bs=128 < input > /dev/nullcon dd bs=64 < input > /dev/null. Degli 0,6 che ci vogliono per leggere quel file, 0,4 sono spesi in quelle readchiamate di sistema nei miei test, mentre altre shell trascorrono molto meno tempo lì.
Stéphane Chazelas,

Bene, non sembra che tu abbia eseguito una vera analisi delle prestazioni. L'influenza della chiamata di lettura (quando si confrontano diverse dimensioni di lettura) è di circa. L'1% di tutto il tempo mentre le funzioni readwc() e trim()in Burne Shell occupano il 30% di tutto il tempo e questo è probabilmente sottostimato in quanto non vi è alcuna libc con gprofannotazione per mbtowc().
schily,

A quale si \xespande?
Mohammad,

11

Il problema non riguarda cate echoriguarda la variabile di citazione dimenticata $i.

Nello script shell tipo Bourne (tranne zsh), lasciare le variabili non quotate causa glob+splitoperatori sulle variabili.

$var

è effettivamente:

glob(split($var))

Quindi, con ogni iterazione di loop, l'intero contenuto di input(esclude le nuove righe finali) verrà espanso, suddiviso, sconvolgente. L'intero processo richiede alla shell di allocare memoria, analizzando la stringa ancora e ancora. Questo è il motivo per cui hai ottenuto una performance negativa.

Puoi citare la variabile per prevenirla, glob+splitma non ti sarà di grande aiuto, poiché quando la shell deve ancora costruire l'argomento della stringa grande e scansionare il suo contenuto per echo(Sostituire builtin echocon external /bin/echoti darà la lista degli argomenti troppo a lungo o senza memoria dipende dalla $idimensione). La maggior parte echodell'implementazione non è conforme a POSIX, espanderà le \xsequenze di barre rovesciate negli argomenti che ha ricevuto.

Con cat, la shell deve solo generare un processo ogni iterazione di loop e catfarà gli I / O della copia. Il sistema può anche memorizzare nella cache il contenuto del file per velocizzare il processo del gatto.


2
@roaima: non hai menzionato la parte globale, che può essere una ragione enorme, immaginando qualcosa che /*/*/*/*../../../../*/*/*/*/../../../../può essere nel contenuto del file. Voglio solo sottolineare i dettagli .
cuonglm,

Gotcha grazie. Anche senza questo, il tempismo raddoppia quando si utilizza una variabile non quotata
roaima,

1
time echo $( <xdditg106) >/dev/null real 0m0.125s user 0m0.085s sys 0m0.025s time echo "$( <xdditg106)" >/dev/null real 0m0.047s user 0m0.016s sys 0m0.022s
Netmonk,

Non ho capito perché la citazione non possa risolvere il problema. Ho bisogno di più descrizione.
Mohammad,

1
@ mohammad.k: Come ho scritto nella mia risposta, la variabile quote impedisce la glob+splitparte e accelera il ciclo while. E ho anche notato che non ti aiuterà molto. Da quando la maggior parte del echocomportamento della shell non è conforme a POSIX. printf '%s' "$i"è meglio.
cuonglm,

2

Se chiami

i=`cat input`

questo consente al processo di shell di crescere da 50 MB fino a 200 MB (a seconda dell'implementazione interna dei caratteri di grandi dimensioni). Ciò può rallentare la shell ma questo non è il problema principale.

Il problema principale è che il comando sopra ha bisogno di leggere l'intero file nella memoria della shell e di echo $idover dividere il campo sul contenuto del file $i. Per eseguire la suddivisione dei campi, tutto il testo del file deve essere convertito in caratteri ampi ed è qui che si trascorre la maggior parte del tempo.

Ho fatto alcuni test con il caso lento e ho ottenuto questi risultati:

  • Il più veloce è ksh93
  • Il prossimo è il mio Bourne Shell (2x più lento di ksh93)
  • Il prossimo è bash (3 volte più lento di ksh93)
  • L'ultimo è ksh88 (7 volte più lento di ksh93)

Il motivo per cui ksh93 è il più veloce sembra essere che ksh93 non usi mbtowc()da libc ma piuttosto una propria implementazione.

A proposito: Stephane si sbaglia che la dimensione di lettura ha una certa influenza, ho compilato la Bourne Shell per leggere blocchi di 4096 byte anziché 128 byte e ho ottenuto le stesse prestazioni in entrambi i casi.


Il i=`cat input`comando non esegue la suddivisione del campo, è quello echo $iche fa. Il tempo trascorso i=`cat input`sarà trascurabile rispetto a echo $i, ma non rispetto al cat inputsolo, e nel caso di bash, la differenza è per la parte migliore a causa di bashletture di piccole dimensioni. Il passaggio da 128 a 4096 non avrà alcuna influenza sulle prestazioni di echo $i, ma non era questo il punto che stavo sollevando.
Stéphane Chazelas,

Si noti inoltre che le prestazioni di echo $ivariano notevolmente a seconda del contenuto dell'input e del filesystem (se contiene caratteri IFS o glob), motivo per cui non ho fatto alcun confronto di shell su quello nella mia risposta. Ad esempio, qui sull'output di yes | ghead -c50M, ksh93 è il più lento di tutti, ma acceso yes | ghead -c50M | paste -sd: -, è il più veloce.
Stéphane Chazelas,

Quando parlavo del tempo totale, stavo parlando dell'intera implementazione e sì, ovviamente la divisione del campo avviene con il comando echo. ed è qui che passa gran parte del tempo.
schily,

Hai ovviamente ragione sul fatto che le prestazioni dipendono dal contenuto di $ i.
schily,

1

In entrambi i casi, il ciclo verrà eseguito solo due volte (una volta per la parola seqe una volta per la parola 10).

Inoltre entrambi uniranno gli spazi bianchi adiacenti e lasceranno cadere gli spazi iniziali / finali, in modo che l'output non sia necessariamente due copie dell'input.

Primo

#!/bin/sh
for j in $(seq 10); do
    cat input
done >> output

Secondo

#!/bin/sh
i="$(cat input)"
for j in $(seq 10); do
    echo "$i"
done >> output

Uno dei motivi per cui echoè più lento potrebbe essere che la tua variabile non quotata viene suddivisa in spazi bianchi in parole separate. Per 50 MB sarà un sacco di lavoro. Cita le variabili!

Ti suggerisco di correggere questi errori e di rivalutare i tuoi tempi.


L'ho provato localmente. Ho creato un file da 50 MB utilizzando l'output di tar cf - | dd bs=1M count=50. Ho anche esteso i loop per essere eseguito da un fattore di x100 in modo che i tempi fossero ridimensionati su un valore ragionevole (ho aggiunto un ulteriore ciclo attorno al tuo intero codice: for k in $(seq 100); do... done). Ecco i tempi:

time ./1.sh

real    0m5.948s
user    0m0.012s
sys     0m0.064s

time ./2.sh

real    0m5.639s
user    0m4.060s
sys     0m0.224s

Come puoi vedere non c'è alcuna differenza reale, ma se qualcosa la versione che contiene echofunziona in modo leggermente più veloce. Se rimuovo le virgolette ed eseguo la tua versione non funzionante 2 il tempo raddoppia, dimostrando che la shell deve fare molto più lavoro del previsto.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s

In realtà il ciclo viene eseguito 10 volte, non due volte.
fpmurphy,

Ho fatto come hai detto, ma il problema non è stato risolto. catè molto, molto più veloce di echo. Il primo script viene eseguito in media 3 secondi, ma il secondo viene eseguito in media 54 secondi.
Mohammad,

@ fpmurphy1: No. Ho provato il mio codice. Il ciclo funziona solo due volte, non 10 volte.
Mohammad,

@ mohammad.k per la terza volta: se si citano le variabili, il problema scompare.
roaima,

@roaima: cosa fa il comando tar cf - | dd bs=1M count=50? Crea un file normale con gli stessi caratteri al suo interno? In tal caso, nel mio caso il file di input è completamente irregolare con tutti i tipi di caratteri e spazi bianchi. E ancora, ho usato timecome hai usato tu, e il risultato è stato quello che ho detto: 54 secondi contro 3 secondi.
Mohammad,

-1

read è molto più veloce di cat

Penso che tutti possano provarlo:

$ cd /sys/devices/system/cpu/cpu0/cpufreq
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do read p < scaling_cur_freq ; done

real    0m0.232s
user    0m0.139s
sys     0m0.088s
───────────────────────────────────────────────────────────────────────────────────────────
$ time for ((i=0; i<10000; i++ )); do cat scaling_cur_freq > /dev/null ; done

real    0m9.372s
user    0m7.518s
sys     0m2.435s
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a read
read is a shell builtin
───────────────────────────────────────────────────────────────────────────────────────────
$ type -a cat
cat is /bin/cat

catrichiede 9.372 secondi. echorichiede .232pochi secondi.

readè 40 volte più veloce .

Il mio primo test quando è $pstato fatto eco allo schermo rivelato è readstato 48 volte più veloce di cat.


-2

Lo echoscopo di mettere 1 linea sullo schermo. Quello che fai nel secondo esempio è che metti il ​​contenuto del file in una variabile e poi stampi quella variabile. Nel primo metti immediatamente il contenuto sullo schermo.

catè ottimizzato per questo utilizzo. echonon è. Anche inserire 50 Mb in una variabile d'ambiente non è una buona idea.


Curioso. Perché non dovrebbe echoessere ottimizzato per la scrittura di testo?
roaima,

2
Non c'è nulla nello standard POSIX che dice che l'eco è pensato per mettere una riga su uno schermo.
fpmurphy,

-2

Non si tratta di eco più veloce, si tratta di quello che stai facendo:

In un caso stai leggendo da input e scrivendo direttamente all'output. In altre parole, qualunque cosa sia letta dall'input attraverso cat, va in output attraverso stdout.

input -> output

Nell'altro caso stai leggendo dall'input in una variabile in memoria e quindi scrivendo il contenuto della variabile in output.

input -> variable
variable -> output

Quest'ultimo sarà molto più lento, specialmente se l'ingresso è di 50 MB.


Penso che devi menzionare che il gatto deve aprire il file oltre a copiarlo da stdin e scriverlo su stdout. Questa è l'eccellenza della seconda sceneggiatura, ma la prima è molto migliore della seconda.
Mohammad,

Non c'è eccellenza nella seconda sceneggiatura; cat deve aprire il file di input in entrambi i casi. Nel primo caso lo stdout di cat va direttamente al file. Nel secondo caso lo stdout di cat passa prima a una variabile, quindi si stampa la variabile nel file di output.
Aleksander,

@ mohammad.k, non c'è enfasi alcuna "eccellenza" nel secondo script.
Carattere jolly
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.