Por que existe tanta diferença no tempo de execução de eco e gato?


15

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 catque 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 iera 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 echoe catcomparei-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 manual echo, 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 como i. No entanto, isso é apenas uma suposição.)
  • E por que tenho problemas quando uso echo?

ATUALIZAR

Eu usei em seq 10vez 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: catfunciona muito mais rápido que echo.


1
e sobre o quê cat $(for i in $(seq 1 10); do echo "input"; done) >> output? :)
netmonk

2
O 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.
roaima 17/09/2015

Citar as variáveis ​​não é o problema; o problema é a variável i (por exemplo, usá-la como um passo intermediário entre entrada e saída).
Aleksander

`echo $ i` - não faça isso. Use printf e cite o argumento.
PSKocik 17/09/2015

1
@PSkocik O que estou dizendo é que você quer printf '%s' "$i", não echo $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/…
PSkocik

Respostas:


24

Há várias coisas a considerar aqui.

i=`cat input`

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 ivariá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 bashser 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.

zshprecisa executar algum pós-processamento para escapar de bytes NUL (outros shells quebram em bytes NUL) e yashfaz 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, echoestá 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 $IFScaracteres na sequência. Se $IFScontiver 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 ( bashde 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 \xsequê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 catnã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. catlerá 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 printfvez de echo).

Se você deseja otimizá-lo ainda mais, em vez de chamar catvárias vezes, basta passar inputvárias vezes para cat.

yes input | head -n 100 | xargs cat

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:

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

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 printfnão está embutido) com:

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

Mas isso nunca será mais eficiente do que usar catno loop (a menos que a entrada seja muito pequena).


Vale ressaltar que, em caso de longa discussão, você pode ficar sem memória . Exemplo/bin/echo $(perl -e 'print "A"x999999')
cuonglm 17/09/2015

Você está enganado com a suposição de que o tamanho da leitura tem uma influência significativa; portanto, leia minha resposta para entender o motivo real.
schily 17/09/2015

@ Schily, fazer 409600 leituras de 128 bytes leva mais tempo (hora do sistema) do que 800 leituras de 64k. Compare dd bs=128 < input > /dev/nullcom dd bs=64 < input > /dev/null. Dos 0,6s necessários para o bash para ler esse arquivo, 0,4 são gastos nessas readchamadas de sistema em meus testes, enquanto outros shells passam muito menos tempo lá.
Stéphane Chazelas

Bem, você não parece ter executado uma análise de desempenho real. A influência da chamada de leitura (ao comparar diferentes tamanhos de leitura) é aprox. 1% do tempo todo, enquanto as funções readwc() e trim()no Burne Shell ocupam 30% do tempo todo, e isso provavelmente é subestimado, pois não há libc com gprofanotações para mbtowc().
schily 17/09/2015

Para qual é \xexpandido?
Mohammad

11

O problema não é cate echoé sobre a variável de cotação esquecida $i.

No shell script do tipo Bourne (exceto zsh), deixar variáveis ​​sem aspas causa glob+splitoperadores nas variáveis.

$var

é na verdade:

glob(split($var))

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+splitmas isso não ajudará muito, pois quando o shell ainda precisar criar o argumento de cadeia grande e verificar seu conteúdo echo(Substituir builtin echopor external /bin/echofornecerá a lista de argumentos muito longa ou sem memória depende do $itamanho). A maior parte da echoimplementação não é compatível com POSIX, ela expandirá as \xseqüências de barra invertida nos argumentos recebidos.

Com cat, o shell precisa gerar um processo a cada iteração de loop e catfará 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.


2
@roaima: Você não mencionou a parte da glob, que pode ser um motivo enorme, imaginando algo que /*/*/*/*../../../../*/*/*/*/../../../../pode estar no conteúdo do arquivo. Só quero apontar os detalhes .
cuonglm

Te agradeço. Mesmo sem isso, o tempo dobra ao usar uma variável não
citada

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

Não entendi por que a citação não pode resolver o problema. Eu preciso de mais descrição.
Mohammad

1
@ mohammad.k: Como escrevi na minha resposta, a variável de citação evita glob+splitparte, e isso acelerará o loop while. E também observei que isso não ajudará muito. Desde quando a maioria do echocomportamento do shell não é compatível com POSIX. printf '%s' "$i"é melhor.
cuonglm

2

Se você ligar

i=`cat input`

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 $iprecisa 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:

  • O mais rápido é ksh93
  • Em seguida é o meu Bourne Shell (2x mais lento que ksh93)
  • A seguir, o bash (3x mais lento que o ksh93)
  • O último é ksh88 (7x mais lento que ksh93)

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.


O i=`cat input`comando não faz divisão de campos, é o echo $ique faz. O tempo gasto i=`cat input`será insignificante comparado a echo $i, mas não comparado cat inputsozinho, e, no caso de bash, a diferença é a melhor parte devido a bashpequenas leituras. Mudar de 128 para 4096 não terá influência no desempenho de echo $i, mas esse não era o ponto que eu estava fazendo.
Stéphane Chazelas

Observe também que o desempenho de echo $ivariará 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 de yes | ghead -c50M, ksh93 é o mais lento de todos, mas yes | ghead -c50M | paste -sd: -o mais rápido.
Stéphane Chazelas

Ao falar sobre o tempo total, eu estava falando sobre toda a implementação e sim, é claro que a divisão do campo acontece com o comando echo. e é aqui que passa a maior parte do tempo.
schily 17/09/2015

Obviamente, você está certo de que o desempenho depende do conteúdo de $ i.
Schilly

1

Nos dois casos, o loop será executado apenas duas vezes (uma vez para a palavra seqe outra para a palavra 10).

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

#!/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

Uma razão pela qual a echovelocidade é 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:

time ./1.sh

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

time ./2.sh

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

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.

time ./2original.sh

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

Na verdade, o loop é executado 10 vezes, não duas vezes.
fpmurphy

Fiz o que você disse, mas o problema não foi resolvido. caté muito, muito mais rápido que echo. O primeiro script é executado em média 3 segundos, mas o segundo é executado em média 54 segundos.
Mohammad

@ fpmurphy1: Não. Eu tentei meu código. O loop é executado apenas duas vezes, não 10 vezes.
Mohammad

@ mohammad.k pela terceira vez: se você citar suas variáveis, o problema desaparece.
roaima 17/09/2015

@roaima: O que o comando 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 usei timecomo você usou, e o resultado foi o que eu disse: 54 segundos vs 3 segundos.
Mohammad

-1

read é muito mais rápido que cat

Eu acho que todos podem testar isso:

$ 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

catleva 9,372 segundos. echoleva .232segundos.

readé 40 vezes mais rápido .

Meu primeiro teste quando $pecoou na tela revelada readfoi 48 vezes mais rápido que cat.


-2

O echoobjetivo é 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. echonão é. Também colocar 50Mb em uma variável de ambiente não é uma boa ideia.


Curioso. Por que não echoseria otimizado para escrever texto?
roaima 17/09/2015

2
Não há nada no padrão POSIX que diga que eco é destinado a colocar uma linha na tela.
fpmurphy

-2

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.

input -> output

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.

input -> variable
variable -> output

O último será muito mais lento, especialmente se a entrada for de 50 MB.


Eu acho que você precisa mencionar que o gato precisa abrir o arquivo, além de copiar do stdin e gravá-lo no stdout. Essa é a excelência do segundo script, mas o primeiro é muito melhor que o segundo no total.
Mohammad

Não há excelência no segundo roteiro; O gato precisa abrir o arquivo de entrada nos dois casos. No primeiro caso, o stdout do gato vai diretamente para o arquivo. No segundo caso, o stdout do gato vai primeiro para uma variável e, em seguida, você imprime a variável no arquivo de saída.
Aleksander

@ mohammad.k, não há enfaticamente nenhuma "excelência" no segundo script.
Curinga
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.