Responder a essa pergunta me fez fazer outra pergunta:
Eu pensei que os seguintes scripts fazem a mesma coisa e o segundo deve ser muito mais rápido, porque o primeiro usa o cat
que precisa abrir o arquivo repetidamente, mas o segundo abre apenas o arquivo uma vez e depois apenas ecoa uma variável:
(Consulte a seção de atualização para obter o código correto.)
Primeiro:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
Segundo:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
enquanto a entrada é de cerca de 50 megabytes.
Mas quando tentei o segundo, era muito, muito lento, porque repetir a variável i
era um processo massivo. Também tive alguns problemas com o segundo script, por exemplo, o tamanho do arquivo de saída foi menor que o esperado.
Também verifiquei a página de manual echo
e cat
comparei-as:
eco - exibe uma linha de texto
cat - concatena arquivos e imprime na saída padrão
Mas eu não entendi a diferença.
Então:
- Por que o gato é tão rápido e o eco é tão lento no segundo script?
- Ou é o problema com variável
i
? (porque, na página de manualecho
, diz que ele exibe "uma linha de texto" e, portanto, acho que é otimizado apenas para variáveis curtas, não para variáveis muito muito longas comoi
. No entanto, isso é apenas uma suposição.) - E por que tenho problemas quando uso
echo
?
ATUALIZAR
Eu usei em seq 10
vez de `seq 10`
incorretamente. Este é o código editado:
Primeiro:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
Segundo:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
(Agradecimentos especiais a roaima .)
No entanto, este não é o objetivo do problema. Mesmo que o loop ocorra apenas uma vez, recebo o mesmo problema: cat
funciona muito mais rápido que echo
.
cat $(for i in $(seq 1 10); do echo "input"; done) >> output
? :)echo
é mais rápido. O que você está perdendo é que você está fazendo o shell fazer muito trabalho, não citando as variáveis quando você as usa.printf '%s' "$i"
, nãoecho $i
. @cuonglm explica alguns dos problemas de eco bem em sua resposta. Para saber por que até a citação não é suficiente em alguns casos com eco, consulte unix.stackexchange.com/questions/65803/…Respostas:
Há várias coisas a considerar aqui.
pode ser caro e há muitas variações entre as conchas.
Esse é um recurso chamado substituição de comando. A idéia é armazenar toda a saída do comando menos os caracteres de nova linha à direita na
i
variável na memória.Para fazer isso, os shells dividem o comando em um subshell e leem sua saída através de um cano ou soquete. Você vê muita variação aqui. Em um arquivo de 50MiB aqui, posso ver, por exemplo, o bash sendo 6 vezes mais lento que o ksh93, mas um pouco mais rápido que o zsh e duas vezes mais rápido que
yash
.O principal motivo para
bash
ser lento é que ele lê a partir do canal 128 bytes por vez (enquanto outros shells lêem 4KiB ou 8KiB por vez) e é penalizado pela sobrecarga de chamada do sistema.zsh
precisa executar algum pós-processamento para escapar de bytes NUL (outros shells quebram em bytes NUL) eyash
faz um processamento ainda mais pesado analisando caracteres de vários bytes.Todos os shells precisam retirar os caracteres de nova linha à direita, que eles podem estar executando de maneira mais ou menos eficiente.
Alguns podem querer lidar com bytes NUL mais graciosamente que outros e verificar sua presença.
Então, uma vez que você tenha essa grande variável na memória, qualquer manipulação nela geralmente envolve alocar mais memória e lidar com dados.
Aqui, você está passando (pretendia passar) o conteúdo da variável para
echo
.Felizmente,
echo
está embutido no seu shell, caso contrário, a execução provavelmente falharia com um erro muito longo da lista arg . Mesmo assim, construir a matriz da lista de argumentos possivelmente envolverá a cópia do conteúdo da variável.O outro problema principal em sua abordagem de substituição de comando é que você está chamando o operador split + glob (esquecendo de citar a variável).
Para isso, os shells precisam tratar a sequência como uma sequência de caracteres (embora alguns shells não o façam e são problemáticos a esse respeito), portanto, nos locais UTF-8, isso significa analisar as sequências UTF-8 (se ainda não o fez
yash
) , procure$IFS
caracteres na sequência. Se$IFS
contiver espaço, tabulação ou nova linha (que é o caso por padrão), o algoritmo é ainda mais complexo e caro. Em seguida, as palavras resultantes dessa divisão precisam ser alocadas e copiadas.A parte glob será ainda mais cara. Se qualquer uma dessas palavras conter caracteres glob (
*
,?
,[
), então o shell terá que ler o conteúdo de alguns diretórios e fazer alguma correspondência de padrão caro (bash
de implementação, por exemplo, é notoriamente muito ruim em que).Se a entrada contiver algo como
/*/*/*/../../../*/*/*/../../../*/*/*
, isso será extremamente caro, pois significa listar milhares de diretórios e pode expandir para várias centenas de MiB.Em seguida
echo
, normalmente fará algum processamento extra. Algumas implementações expandem\x
sequências no argumento recebido, o que significa analisar o conteúdo e provavelmente outra alocação e cópia dos dados.Por outro lado, OK, na maioria dos shells
cat
não é incorporado, o que significa bifurcar um processo e executá-lo (carregar o código e as bibliotecas), mas após a primeira chamada, esse código e o conteúdo do arquivo de entrada será armazenado em cache na memória. Por outro lado, não haverá intermediário.cat
lerá grandes quantidades de cada vez e gravará imediatamente, sem processamento, e não precisará alocar uma quantidade enorme de memória, apenas aquele buffer que ele reutiliza.Isso também significa que é muito mais confiável, pois não engasga com bytes NUL e não corta caracteres de nova linha à direita (e não divide + glob, embora você possa evitar isso citando a variável e não expanda a sequência de escape, embora você possa evitá-la usando em
printf
vez deecho
).Se você deseja otimizá-lo ainda mais, em vez de chamar
cat
várias vezes, basta passarinput
várias vezes paracat
.Irá executar 3 comandos em vez de 100.
Para tornar a versão variável mais confiável, você precisará usar
zsh
(outros shells não podem lidar com bytes NUL) e fazer isso:Se você souber que a entrada não contém bytes NUL, poderá fazê-lo de maneira confiável POSIXly (embora possa não funcionar onde
printf
não está embutido) com:Mas isso nunca será mais eficiente do que usar
cat
no loop (a menos que a entrada seja muito pequena).fonte
/bin/echo $(perl -e 'print "A"x999999')
dd bs=128 < input > /dev/null
comdd bs=64 < input > /dev/null
. Dos 0,6s necessários para o bash para ler esse arquivo, 0,4 são gastos nessasread
chamadas de sistema em meus testes, enquanto outros shells passam muito menos tempo lá.readwc()
etrim()
no Burne Shell ocupam 30% do tempo todo, e isso provavelmente é subestimado, pois não há libc comgprof
anotações parambtowc()
.\x
expandido?O problema não é
cat
eecho
é sobre a variável de cotação esquecida$i
.No shell script do tipo Bourne (exceto
zsh
), deixar variáveis sem aspas causaglob+split
operadores nas variáveis.é na verdade:
Portanto, com cada iteração de loop, todo o conteúdo de
input
(excluir novas linhas à direita) será expandido, dividido e globbing. Todo o processo requer que o shell aloque memória, analisando a sequência repetidamente. Essa é a razão pela qual você teve um desempenho ruim.Você pode citar a variável para evitar,
glob+split
mas isso não ajudará muito, pois quando o shell ainda precisar criar o argumento de cadeia grande e verificar seu conteúdoecho
(Substituir builtinecho
por external/bin/echo
fornecerá a lista de argumentos muito longa ou sem memória depende do$i
tamanho). A maior parte daecho
implementação não é compatível com POSIX, ela expandirá as\x
seqüências de barra invertida nos argumentos recebidos.Com
cat
, o shell precisa gerar um processo a cada iteração de loop ecat
fará a cópia de E / S. O sistema também pode armazenar em cache o conteúdo do arquivo para acelerar o processo do gato.fonte
/*/*/*/*../../../../*/*/*/*/../../../../
pode estar no conteúdo do arquivo. Só quero apontar os detalhes .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
glob+split
parte, e isso acelerará o loop while. E também observei que isso não ajudará muito. Desde quando a maioria doecho
comportamento do shell não é compatível com POSIX.printf '%s' "$i"
é melhor.Se você ligar
isso permite que o processo do shell cresça de 50 MB a 200 MB (dependendo da implementação interna de caracteres amplos). Isso pode tornar seu shell lento, mas esse não é o principal problema.
O principal problema é que o comando acima precisa ler o arquivo inteiro na memória shell e
echo $i
precisa dividir o campo no conteúdo desse arquivo$i
. Para fazer a divisão de campos, todo o texto do arquivo precisa ser convertido em caracteres largos e é aqui que passa a maior parte do tempo.Fiz alguns testes com o caso lento e obtive estes resultados:
A razão pela qual o ksh93 é o mais rápido parece ser que o ksh93 não usa
mbtowc()
da libc, mas sim uma implementação própria.BTW: Stephane está enganado que o tamanho da leitura tenha alguma influência. Compilei o Bourne Shell para ler em pedaços de 4096 bytes em vez de 128 bytes e obtive o mesmo desempenho nos dois casos.
fonte
i=`cat input`
comando não faz divisão de campos, é oecho $i
que faz. O tempo gastoi=`cat input`
será insignificante comparado aecho $i
, mas não comparadocat input
sozinho, e, no caso debash
, a diferença é a melhor parte devido abash
pequenas leituras. Mudar de 128 para 4096 não terá influência no desempenho deecho $i
, mas esse não era o ponto que eu estava fazendo.echo $i
variará consideravelmente, dependendo do conteúdo da entrada e do sistema de arquivos (se ele contiver caracteres IFS ou glob), e é por isso que não fiz nenhuma comparação de shells na minha resposta. Por exemplo, aqui na saída deyes | ghead -c50M
, ksh93 é o mais lento de todos, masyes | ghead -c50M | paste -sd: -
o mais rápido.Nos dois casos, o loop será executado apenas duas vezes (uma vez para a palavra
seq
e outra para a palavra10
).Além disso, ambos mesclarão o espaço em branco adjacente e eliminarão o espaço em branco inicial / final, de modo que a saída não seja necessariamente duas cópias da entrada.
Primeiro
Segundo
Uma razão pela qual a
echo
velocidade é mais lenta pode ser que sua variável não citada esteja sendo dividida no espaço em branco em palavras separadas. Para 50 MB, será muito trabalho. Cite as variáveis!Sugiro que você corrija esses erros e reavalie seus horários.
Eu testei isso localmente. Criei um arquivo de 50 MB usando a saída de
tar cf - | dd bs=1M count=50
. Também estendi os loops para serem executados por um fator de x100, para que os tempos fossem redimensionados para um valor razoável (adicionei um loop adicional ao redor de todo o seu código:for k in $(seq 100); do
...done
). Aqui estão os horários:Como você pode ver, não há diferença real, mas, se houver algo que a versão contenha
echo
, execute marginalmente mais rápido. Se eu remover as aspas e executar sua versão quebrada 2, o tempo dobrará, mostrando que o shell está tendo que fazer muito mais trabalho esperado.fonte
cat
é muito, muito mais rápido queecho
. O primeiro script é executado em média 3 segundos, mas o segundo é executado em média 54 segundos.tar cf - | dd bs=1M count=50
faz? Ele cria um arquivo regular com os mesmos caracteres? Nesse caso, no meu caso, o arquivo de entrada é completamente irregular com todos os tipos de caracteres e espaços em branco. E, novamente, eu useitime
como você usou, e o resultado foi o que eu disse: 54 segundos vs 3 segundos.read
é muito mais rápido quecat
Eu acho que todos podem testar isso:
cat
leva 9,372 segundos.echo
leva.232
segundos.read
é 40 vezes mais rápido .Meu primeiro teste quando
$p
ecoou na tela reveladaread
foi 48 vezes mais rápido quecat
.fonte
O
echo
objetivo é colocar uma linha na tela. O que você faz no segundo exemplo é que você coloca o conteúdo do arquivo em uma variável e depois imprime essa variável. No primeiro, você coloca imediatamente o conteúdo na tela.cat
é otimizado para esse uso.echo
não é. Também colocar 50Mb em uma variável de ambiente não é uma boa ideia.fonte
echo
seria otimizado para escrever texto?Não se trata de eco ser mais rápido, é sobre o que você está fazendo:
Em um caso, você está lendo da entrada e escrevendo na saída diretamente. Em outras palavras, o que quer que seja lido da entrada através do gato, vai para a saída através do stdout.
No outro caso, você está lendo da entrada para uma variável na memória e depois escrevendo o conteúdo da variável na saída.
O último será muito mais lento, especialmente se a entrada for de 50 MB.
fonte