In che modo `yes` scrive sul file così rapidamente?


58

Lasciami fare un esempio:

$ timeout 1 yes "GNU" > file1
$ wc -l file1
11504640 file1

$ for ((sec0=`date +%S`;sec<=$(($sec0+5));sec=`date +%S`)); do echo "GNU" >> file2; done
$ wc -l file2
1953 file2

Qui puoi vedere che il comando yesscrive 11504640righe in un secondo mentre posso scrivere solo 1953righe in 5 secondi usando bash fore echo.

Come suggerito nei commenti, ci sono vari trucchi per renderlo più efficiente, ma nessuno si avvicina alla corrispondenza della velocità di yes:

$ ( while :; do echo "GNU" >> file3; done) & pid=$! ; sleep 1 ; kill $pid
[1] 3054
$ wc -l file3
19596 file3

$ timeout 1 bash -c 'while true; do echo "GNU" >> file4; done'
$ wc -l file4
18912 file4

Questi possono scrivere fino a 20 mila righe in un secondo. E possono essere ulteriormente migliorati per:

$ timeout 1 bash -c 'while true; do echo "GNU"; done >> file5' 
$ wc -l file5
34517 file5

$ ( while :; do echo "GNU"; done >> file6 ) & pid=$! ; sleep 1 ; kill $pid
[1] 5690
$ wc -l file6
40961 file6

Questi ci portano fino a 40 mila linee in un secondo. Meglio, ma ancora un grido lontano dal yesquale può scrivere circa 11 milioni di righe in un secondo!

Quindi, come si yesscrive in un file così rapidamente?



9
Nel secondo esempio, hai due invocazioni di comandi esterni per ogni iterazione del loop ed dateè piuttosto pesante, inoltre la shell deve riaprire il flusso di output echoper ogni iterazione di loop. Nel primo esempio, esiste solo una singola chiamata di comando con un singolo reindirizzamento dell'output e il comando è estremamente leggero. I due non sono in alcun modo comparabili.
un CVn

@ MichaelKjörling hai ragione datepotrebbe essere pesante, vedi modifica alla mia domanda.
Pandya,

1
timeout 1 $(while true; do echo "GNU">>file2; done;)è il modo sbagliato di utilizzare timeout poiché il timeoutcomando verrà avviato solo al termine della sostituzione del comando. Usa timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
Muru,

1
riepilogo delle risposte: impiegando solo il tempo della CPU per write(2)le chiamate di sistema, non per i carichi di altre scale di sistema, l'overhead della shell o persino la creazione di processi nel primo esempio (che viene eseguito e attende dateogni riga stampata nel file). Un secondo di scrittura è appena sufficiente per il collo di bottiglia sull'I / O del disco (piuttosto che sulla CPU / memoria), su un sistema moderno con molta RAM. Se fosse permesso di correre più a lungo, la differenza sarebbe più piccola. (A seconda della cattiva implementazione di bash utilizzata e della velocità relativa della CPU e del disco, potresti non saturare l'I / O del disco con bash).
Peter Cordes,

Risposte:


65

poche parole:

yespresenta un comportamento simile alla maggior parte delle altre utility standard che tipicamente scrivono in un FILE STREAM con output bufferizzato dalla libC tramite stdio . Questi fanno solo il syscall write()ogni 4kb (16kb o 64kb) o qualunque sia il blocco di output BUFSIZ . echoè un write()per GNU. Questo è un sacco di cambio di modalità (che non è, apparentemente, costoso come un cambio di contesto ) .

E questo non significa affatto che, oltre al suo ciclo di ottimizzazione iniziale, yessia un ciclo C molto semplice, minuscolo e compilato e il tuo ciclo shell non è in alcun modo paragonabile a un programma ottimizzato per il compilatore.


ma mi sbagliavo:

Quando ho detto prima che yesusava lo stdio, pensavo che lo facesse solo perché si comporta in modo molto simile a quelli che lo fanno. Questo non era corretto - emula solo il loro comportamento in questo modo. Quello che fa in realtà è molto simile a un analogo a quello che ho fatto di seguito con la shell: prima gira per confondere i suoi argomenti (o yse nessuno) fino a quando non potrebbero crescere più senza eccedere BUFSIZ.

Un commento dalla fonte che precede immediatamente i relativi forstati del ciclo:

/* Buffer data locally once, rather than having the
large overhead of stdio buffering each item.  */

yesfa il suo fa da solo in write()seguito.


digressione:

(Come originariamente incluso nella domanda e conservato per il contesto di una possibile spiegazione informativa già scritta qui) :

Ho provato timeout 1 $(while true; do echo "GNU">>file2; done;)ma non sono riuscito a interrompere il loop.

Il timeoutproblema che hai con la sostituzione del comando - Penso di averlo capito ora e posso spiegare perché non si ferma. timeoutnon si avvia perché la riga di comando non viene mai eseguita. La shell crea una shell figlio, apre una pipe sul suo stdout e la legge. Smetterà di leggere quando il bambino smetterà, e poi interpreterà tutto ciò che il bambino ha scritto per l' $IFSespansione di mangling e glob, e con i risultati sostituirà tutto $(dall'abbinamento ).

Ma se il bambino è un ciclo infinito che non scrive mai nella pipe, allora il bambino non smette mai di eseguire il ciclo e timeoutla riga di comando non viene mai completata prima (come immagino) lo fai CTRL-Ce uccidi il ciclo figlio. Quindi nontimeout può mai uccidere il ciclo che deve essere completato prima che possa iniziare.


altri timeouts:

... semplicemente non sono rilevanti per i tuoi problemi di prestazioni come il tempo che il tuo programma shell deve impiegare a passare dalla modalità utente a quella kernel per gestire l'output. timeout, tuttavia, non è flessibile come una shell potrebbe essere per questo scopo: dove le shell eccellono nella loro capacità di manipolare argomenti e gestire altri processi.

Come notato altrove, semplicemente spostando il [fd-num] >> named_filereindirizzamento verso la destinazione di output del loop piuttosto che dirigere solo l'output lì per il comando in loop può migliorare sostanzialmente le prestazioni perché in questo modo almeno la open()syscall deve essere eseguita solo una volta. Questo viene fatto anche di seguito con il |tubo mirato come uscita per i circuiti interni.


confronto diretto:

Potresti fare come:

for cmd in  exec\ yes 'while echo y; do :; done'
do      set +m
        sh  -c '{ sleep 1; kill "$$"; }&'"$cmd" | wc -l
        set -m
done

256659456
505401

Il che è un po ' come la relazione secondaria del comando descritta in precedenza, ma non c'è pipe e il bambino è in background fino a quando non uccide il genitore. Nel yescaso in cui il genitore sia stato effettivamente sostituito da quando il figlio è stato generato, ma la shell chiama yessovrapponendo il proprio processo con quello nuovo e quindi il PID rimane lo stesso e il suo figlio di zombi sa ancora chi uccidere dopo tutto.


buffer più grande:

Ora vediamo come aumentare il write()buffer della shell .

IFS="
";    set y ""              ### sets up the macro expansion       
until [ "${512+1}" ]        ### gather at least 512 args
do    set "$@$@";done       ### exponentially expands "$@"
printf %s "$*"| wc -c       ### 1 write of 512 concatenated "y\n"'s  

1024

Ho scelto quel numero perché le stringhe di output più lunghe di 1kb venivano divise in due separate write()per me. E quindi ecco di nuovo il ciclo:

for cmd in 'exec  yes' \
           'until [ "${512+:}" ]; do set "$@$@"; done
            while printf %s "$*"; do :; done'
do      set +m
        sh  -c $'IFS="\n"; { sleep 1; kill "$$"; }&'"$cmd" shyes y ""| wc -l
        set -m
done

268627968
15850496

È 300 volte la quantità di dati scritti dalla shell nello stesso lasso di tempo per questo test rispetto all'ultimo. Non troppo malandato. Ma non lo è yes.


relazionato:

Come richiesto, esiste una descrizione più completa dei semplici commenti in codice su ciò che viene fatto qui a questo link .


@heemayl - forse? non sono del tutto sicuro di aver capito cosa stai chiedendo? quando un programma usa stdio per scrivere l'output lo fa senza buffering (come stderr di default) o buffering di linea (ai terminali di default) o buffering a blocchi (praticamente la maggior parte delle cose è impostata in questo modo di default) . Sono un po 'poco chiaro su ciò che imposta la dimensione del buffer di output - ma di solito è un po' di 4kb. e così le funzioni di stdio lib raccoglieranno il loro output fino a quando non saranno in grado di scrivere un intero blocco. ddè uno strumento standard che sicuramente non usa stdio, per esempio. molti altri lo fanno.
Mikeserv,

3
La versione della shell sta eseguendo open(esistente) writeE close(che credo sia ancora in attesa di flush), E creando un nuovo processo ed esecuzione date, per ogni ciclo.
dave_thompson_085,

@ dave_thompson_085 - vai su / dev / chat . e quello che dici non è necessariamente vero, come puoi vedere lì. Ad esempio, fare quel wc -lloop con bashper me ottiene 1/5 dell'output del shloop - bashgestisce da poco più di 100k writes()a dash500k.
Mikeserv,

Mi dispiace, ero ambiguo; Intendevo la versione shell nella domanda, che al momento in cui avevo letto aveva solo la versione originale con il for((sec0=`date +%S`;...tempo di controllo e il reindirizzamento nel ciclo, non i successivi miglioramenti.
dave_thompson_085,

@ dave_thompson_085 - va bene. la risposta era sbagliata comunque su alcuni punti fondamentali e ora dovrebbe essere praticamente corretta, come spero.
Mikeserv,

20

Una domanda migliore sarebbe perché la tua shell sta scrivendo il file così lentamente. Qualsiasi programma compilato autonomo che utilizza responsabilmente syscalls di scrittura di file (non svuotare ogni carattere alla volta) lo farebbe abbastanza rapidamente. Quello che stai facendo è scrivere linee in un linguaggio interpretato (la shell) e inoltre fai molte operazioni di input input non necessarie. Cosa yesfa:

  • apre un file per la scrittura
  • chiama funzioni ottimizzate e compilate per la scrittura su uno stream
  • lo stream è bufferizzato, quindi un syscall (un passaggio costoso alla modalità kernel) avviene molto raramente, in grossi pezzi
  • chiude un file

Cosa fa la tua sceneggiatura:

  • legge una riga di codice
  • interpreta il codice, facendo molte operazioni extra per analizzare effettivamente i tuoi input e capire cosa fare
  • per ogni iterazione di while loop (che probabilmente non è economico in un linguaggio interpretato):
    • chiama il datecomando esterno e memorizza il suo output (solo nella versione originale - nella versione rivista guadagni un fattore 10 non facendolo)
    • verifica se la condizione di terminazione del loop è soddisfatta
    • aprire un file in modalità append
    • echocomando di analisi , riconoscerlo (con un po 'di codice di corrispondenza del modello) come incorporato nella shell, chiamare l'espansione dei parametri e tutto il resto sull'argomento "GNU" e infine scrivere la riga nel file aperto
    • chiudi di nuovo il file
    • ripetere il processo

Le parti costose: l'intera interpretazione è estremamente costosa (bash sta facendo un sacco di preelaborazione di tutto l'input - la tua stringa potrebbe potenzialmente contenere sostituzione variabile, sostituzione di processo, espansione di parentesi graffe, caratteri di escape e altro), ogni chiamata di un builtin è probabilmente un'istruzione switch con reindirizzamento a una funzione che si occupa dell'integrato e, cosa molto importante, apri e chiudi un file per ogni singola riga di output. Potresti mettere >> fileal di fuori del ciclo while per renderlo molto più veloce , ma sei ancora in un linguaggio interpretato. Sei abbastanza fortunatoechoè un comando incorporato della shell, non un comando esterno, altrimenti il ​​tuo ciclo comporterebbe la creazione di un nuovo processo (fork & exec) su ogni singola iterazione. Il che fermerebbe il processo - hai visto quanto è costoso quando hai avuto il datecomando nel loop.


11

Le altre risposte hanno affrontato i punti principali. In una nota a margine, puoi aumentare il throughput del tuo ciclo while scrivendo nel file di output alla fine del calcolo. Confrontare:

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU" >>/tmp/f; done;

real    0m0.080s
user    0m0.032s
sys     0m0.037s

con

$ i=0;time while  [ $i -le 1000 ]; do ((++i)); echo "GNU"; done>>/tmp/f;

real    0m0.030s
user    0m0.019s
sys     0m0.011s

Sì, questo è importante e la velocità di scrittura (almeno) raddoppia nel mio caso
Pandya,
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.