Como o `yes` grava no arquivo tão rapidamente?


58

Deixe-me dar um exemplo:

$ 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

Aqui você pode ver que o comando yesescreve 11504640linhas em um segundo, enquanto eu posso escrever apenas 1953linhas em 5 segundos usando bash fore echo.

Conforme sugerido nos comentários, existem vários truques para torná-lo mais eficiente, mas nenhum chega nem perto de corresponder à velocidade de 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

Eles podem escrever até 20 mil linhas em um segundo. E eles podem ser melhorados ainda mais para:

$ 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

Isso nos leva a 40 mil linhas em um segundo. Melhor, mas ainda muito longe do yesqual pode escrever cerca de 11 milhões de linhas em um segundo!

Então, como yesgravar no arquivo tão rapidamente?



9
No segundo exemplo, você tem duas invocações de comandos externos para cada iteração do loop e dateé um pouco pesado, além disso, o shell precisa reabrir o fluxo de saída echopara cada iteração do loop. No primeiro exemplo, existe apenas uma chamada de comando com um redirecionamento de saída único, e o comando é extremamente leve. Os dois não são de forma alguma comparáveis.
um CVn

@ MichaelKjörling você está certo datepode ser muito pesado, veja editar a minha pergunta.
Pandya

1
timeout 1 $(while true; do echo "GNU">>file2; done;)é a maneira errada de usar, timeout pois o timeoutcomando será iniciado apenas quando a substituição do comando for concluída. Use timeout 1 sh -c 'while true; do echo "GNU">>file2; done'.
muru

1
resumo das respostas: gastando apenas o tempo da CPU em write(2)chamadas do sistema, não em cargas de barco de outros syscalls, sobrecarga de shell ou até mesmo criação de processo no seu primeiro exemplo (que executa e aguarda datepor cada linha impressa no arquivo). Um segundo de gravação é suficiente para afunilar a E / S do disco (em vez da CPU / memória), em um sistema moderno com muita RAM. Se for permitido executar mais, a diferença seria menor. (Dependendo do grau de implementação ruim do bash e da velocidade relativa da CPU e do disco, você pode nem saturar a E / S do disco com o bash).
Peter Cordes

Respostas:


65

casca de noz:

yesexibe comportamento semelhante à maioria dos outros utilitários padrão que normalmente gravam em um FILE STREAM com saída em buffer pelo libC via stdio . Eles fazem o syscall somente a write()cada 4kb (16kb ou 64kb) ou qualquer que seja o bloco de saída BUFSIZ . echoé um write()por GNU. É muita troca de modo (o que aparentemente não é tão caro quanto uma troca de contexto ) .

E isso não é nada para mencionar que, além de seu loop de otimização inicial, yesé um loop C muito simples, minúsculo e compilado e seu loop de shell não é de forma alguma comparável a um programa otimizado para compilador.


mas eu estava errado:

Quando eu disse antes que o yesstdio usado, eu apenas assumi que sim porque se comporta muito como aqueles que o fazem. Isso não estava correto - apenas emula o comportamento deles dessa maneira. Na verdade, o que ele faz é muito parecido com o que fiz abaixo com o shell: primeiro ele faz um loop para confundir seus argumentos (ou ynenhum) até que eles não cresçam mais sem exceder BUFSIZ.

Um comentário da fonte imediatamente anterior ao forloop relevante indica:

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

yesfaz o que faz a si próprio write()depois disso.


digressão:

(Como originalmente incluído na pergunta e retido por contexto para uma explicação possivelmente informativa já escrita aqui) :

Eu tentei, timeout 1 $(while true; do echo "GNU">>file2; done;)mas incapaz de parar o loop.

O timeoutproblema que você tem com a substituição de comando - acho que entendi agora e posso explicar por que não para. timeoutnão inicia porque sua linha de comando nunca é executada. Seu shell bifurca um shell filho, abre um tubo no stdout e o lê. Ele irá parar de ler quando a criança sair e, em seguida, interpretará toda a criança escrita para $IFSexpansões desconcertantes e globais e, com os resultados, substituirá tudo, desde $(a correspondência ).

Mas se o filho é um loop sem fim que nunca grava no canal, ele nunca para de fazer um loop e timeouta linha de comando nunca é concluída antes (como eu acho) de você fazer CTRL-Ce matar o loop. Portanto, nunca étimeout possível eliminar o loop que precisa ser concluído antes de iniciar.


outros timeouts:

... simplesmente não são tão relevantes para seus problemas de desempenho quanto a quantidade de tempo que seu programa shell deve gastar alternando entre os modos de usuário e kernel para lidar com a saída. timeout, no entanto, não é tão flexível quanto um shell pode ser para esse propósito: onde os shells se destacam tem a capacidade de manipular argumentos e gerenciar outros processos.

Como observado em outro lugar, simplesmente mover o [fd-num] >> named_fileredirecionamento para o destino de saída do loop em vez de direcionar a saída para o comando em loop pode melhorar substancialmente o desempenho, pois dessa forma pelo menos o open()syscall precisa ser feito apenas uma vez. Isso também é feito abaixo, com o |tubo direcionado como saída para os loops internos.


comparação direta:

Você pode fazer como:

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

Que é tipo de como a relação de comando sub descrito antes, mas não há nenhuma tubulação e a criança está em segundo plano até que ele mata o pai. No yescaso, o pai foi realmente substituído desde que a criança foi criada, mas o shell chama yessobrepondo seu próprio processo com o novo e, assim, o PID permanece o mesmo e seu filho zumbi ainda sabe quem matar, afinal.


buffer maior:

Agora vamos ver como aumentar o write()buffer do 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

Eu escolhi esse número porque as seqüências de saída com mais de 1kb foram divididas em write()s separadas para mim. E aqui está o loop novamente:

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

Isso representa 300 vezes a quantidade de dados gravados pelo shell na mesma quantidade de tempo para este teste que o último. Não é muito pobre. Mas não é yes.


relacionados:

Conforme solicitado, há uma descrição mais completa do que os meros comentários de código sobre o que é feito aqui neste link .


@heemayl - talvez? Eu não tenho certeza se entendi o que você está perguntando? quando um programa usa o stdio para gravar a saída, ele faz sem buffer (como stderr por padrão) ou buffer de linha (para terminais por padrão) ou buffer de bloco (basicamente a maioria das outras coisas é definida dessa maneira por padrão) . eu sou um pouco incerto sobre o que define o tamanho do buffer de saída - mas geralmente é de 4kb. e assim as funções stdio lib reunirão sua saída até que possam escrever um bloco inteiro. ddé uma ferramenta padrão que definitivamente não usa stdio, por exemplo. a maioria dos outros faz.
mikeserv

3
A versão do shell está executando AND open(existente) writeAND close(que eu acredito que ainda aguarda liberação), AND criando um novo processo e executando date, para cada loop.
dave_thompson_085

@ dave_thompson_085 - vá para / dev / chat . e o que você diz não é necessariamente verdade, como você pode ver lá. Por exemplo, fazer esse wc -lloop bashcomigo obtém 1/5 da saída do shloop - bashgerencia um pouco mais de 100k writes()a dash500k de s.
mikeserv

Desculpe, eu era ambígua; Eu quis dizer a versão do shell na pergunta, que no momento em que o li tinha apenas a versão original com o for((sec0=`date +%S`;...controle do tempo e o redirecionamento no loop, não as melhorias subseqüentes.
precisa saber é o seguinte

@ dave_thompson_085 - tudo bem. de qualquer maneira, a resposta estava errada sobre alguns pontos fundamentais e deve estar praticamente correta agora, como espero.
precisa saber é

20

Uma pergunta melhor seria por que o seu shell está gravando o arquivo tão lentamente. Qualquer programa compilado independente que use syscalls de gravação de arquivo de forma responsável (sem liberar todos os caracteres de uma vez) faria isso razoavelmente rápido. O que você está fazendo é escrever linhas em uma linguagem interpretada (o shell) e, além disso, você realiza muitas operações desnecessárias de saída de entrada. O que yesfaz:

  • abre um arquivo para gravação
  • chama funções otimizadas e compiladas para gravar em um fluxo
  • o fluxo é armazenado em buffer; portanto, um syscall (uma mudança cara para o modo kernel) acontece muito raramente, em grandes blocos
  • fecha um arquivo

O que seu script faz:

  • lê em uma linha de código
  • interpreta o código, realizando muitas operações extras para analisar sua entrada e descobrir o que fazer
  • para cada iteração do loop while (que provavelmente não é barato em uma linguagem interpretada):
    • chame o datecomando externo e armazene sua saída (somente na versão original - na versão revisada, você ganha um fator de 10 por não fazer isso)
    • testar se a condição de término do loop é atendida
    • abrir um arquivo no modo de acréscimo
    • echocomando parse , reconheça-o (com algum código de correspondência de padrões) como um shell embutido, chame a expansão de parâmetros e tudo mais no argumento "GNU" e, finalmente, escreva a linha no arquivo aberto
    • feche o arquivo novamente
    • repita o processo

As partes caras: toda a interpretação é extremamente cara (o bash está realizando uma enorme quantidade de pré-processamento de todas as entradas - sua string pode conter substituição de variáveis, substituição de processos, expansão de chaves, caracteres de escape e muito mais), todas as chamadas de um built-in são provavelmente uma instrução switch com redirecionamento para uma função que lida com o built-in e, o que é mais importante, você abre e fecha um arquivo para cada linha de saída. Você pode colocar >> filefora do loop while para torná-lo muito mais rápido , mas ainda está em uma linguagem interpretada. Você tem muita sorte queechoé um shell embutido, não um comando externo - caso contrário, seu loop envolveria a criação de um novo processo (fork & exec) em cada iteração. O que interromperia o processo - você viu o quanto isso custaria quando você tinha o datecomando no loop.


11

As outras respostas abordaram os pontos principais. Em uma nota lateral, você pode aumentar a taxa de transferência do seu loop while gravando no arquivo de saída no final do cálculo. Comparar:

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

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

com

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

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

Sim, isso importa e a velocidade de gravação (pelo menos) dobra no meu caso #
Pandya
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.