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

14

Responder esta pergunta me causou 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 cat que precisa abrir o arquivo várias vezes, mas o segundo abre o arquivo apenas uma vez e depois ecoa um variável:

(Veja a seção de atualização para 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 experimentei o segundo, ele também ficou lento demais, porque o eco da variável i foi um processo massivo. Eu também tenho alguns problemas com o segundo script, por exemplo, o tamanho do arquivo de saída foi menor do que o esperado.

Também verifiquei a página do manual de echo e cat para compará-los:

echo - display a line of text

cat - concatenate files and print on the standard output

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 roteiro?
  • Ou o problema com a variável i ? (porque na página man do echo é dito que exibe "uma linha de texto" e então eu acho que é otimizado apenas para variáveis curtas, não para variáveis muito longas como i . No entanto, isso é apenas um palpite.)
  • E por que tenho problemas quando uso echo ?

UPDATE

Eu usei seq 10 em 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 para roaima .)

No entanto, não é o ponto do problema. Mesmo que o loop ocorra apenas uma vez, recebo o mesmo problema: cat funciona muito mais rápido que echo .

    
por Mohammad 17.09.2015 / 08:10

7 respostas

23

Há várias coisas a considerar aqui.

i='cat input'

pode ser caro e há muitas variações entre os shells.

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 variável i na memória.

Para fazer isso, shells bifurcam o comando em um subshell e lêem sua saída através de um pipe ou par de soquetes. Você vê muita variação aqui. Em um arquivo de 50 MiB aqui, eu posso ver por exemplo que o bash é 6 vezes mais lento que o ksh93, mas um pouco mais rápido que o zsh e duas vezes mais rápido que o yash .

A principal razão para o bash ser lento é que ele lê do pipe 128 bytes por vez (enquanto outros shells leem 4KiB ou 8KiB por vez) e é penalizado pela sobrecarga da chamada do sistema.

zsh precisa fazer algum pós-processamento para escapar de bytes NUL (outros shells quebram em bytes NUL), e yash faz processamento ainda mais pesado analisando caracteres de múltiplos bytes.

Todos os shells precisam remover os caracteres de nova linha que podem estar fazendo com mais ou menos eficiência.

Alguns podem querer manipular bytes NUL mais facilmente que outros e verificar sua presença.

Então, quando você tem aquela grande variável na memória, qualquer manipulação geralmente envolve a alocação de mais memória e dados de enfrentamento.

Aqui, você está passando (pretendia passar) o conteúdo da variável para echo .

Por sorte, echo está embutido em seu shell, caso contrário a execução provavelmente teria falhado com um erro arg list too long . Mesmo assim, construir a matriz da lista de argumentos possivelmente envolverá copiar o conteúdo da variável.

O outro grande problema na sua abordagem de substituição de comandos é que você está invocando o operador split + glob (esquecendo-se de citar a variável).

Para isso, os shells precisam tratar a string como uma string de caracteres (apesar de alguns shells não possuírem bugs nesse sentido) então em locales UTF-8, isso significa analisar UTF- 8 sequências (se não forem feitas como yash ), procure por $IFS caracteres na string. Se $IFS contiver espaço, tabulação ou nova linha (que é o caso por padrão), o algoritmo será ainda mais complexo e caro. Então, as palavras resultantes dessa divisão precisam ser alocadas e copiadas.

A parte glob será ainda mais cara. Se qualquer uma dessas palavras contiverem caracteres glob ( * , ? , [ ), o shell terá que ler o conteúdo de alguns diretórios e fazer uma correspondência de padrões cara (a implementação de bash , por exemplo, é notoriamente muito ruim nisso).

Se a entrada contiver algo como /*/*/*/../../../*/*/*/../../../*/*/* , isso será extremamente caro, pois isso significa listar milhares de diretórios e expandir para várias centenas de MiB.

Em seguida, echo normalmente fará algum processamento extra. Algumas implementações expandem \x de 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, o cat não está integrado, o que significa que um processo é executado e executado (carregando 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 quantias por vez e as escreverá imediatamente, sem processamento, e não precisará alocar uma quantidade enorme de memória, apenas um buffer que reutilize.

Isso também significa que é muito mais confiável, já que não engasga com bytes NUL e não corta caracteres de nova linha (e não faz split + glob, embora você possa evitar isso citando a variável, e não expande a sequência de escape, embora você possa evitar isso usando printf em vez de echo ).

Se você quiser otimizá-lo ainda mais, em vez de invocar cat várias vezes, basta passar input várias vezes para cat .

yes input | head -n 100 | xargs cat

executará 3 comandos em vez de 100.

Para tornar a versão da variável mais confiável, você precisará usar zsh (outras shells não podem lidar com bytes NUL) e faça isso:

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

Se você sabe que a entrada não contém bytes NUL, então você pode fazê-lo POSIXly de forma confiável (embora talvez não funcione onde printf nã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 cat no loop (a menos que a entrada seja muito pequena).

    
por 17.09.2015 / 10:54
11

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

Em um shell script parecido com o Bourne (exceto zsh ), deixando as variáveis entre as causas glob+split operadores nas variáveis.

$var

é, na verdade:

glob(split($var))

Assim, a cada iteração de loop, todo o conteúdo de input (excluindo novas linhas) será expandido, dividido, globbing. Todo o processo requer shell para alocar memória, analisando a string novamente e novamente. Essa é a razão pela qual você conseguiu o mau desempenho.

Você pode citar a variável para evitar glob+split , mas isso não ajudará muito, já que quando o shell ainda precisar criar o argumento de string grande e varrer seu conteúdo para echo (Substituindo builtin echo por external /bin/echo dará a lista de argumentos muito longa ou falta de memória depende do $i size). A maior parte da implementação de echo não é compatível com POSIX, mas irá expandir as sequências de barras invertidas \x nos argumentos recebidos.

Com cat , o shell só precisa gerar um processo em cada iteração de loop e cat fará a cópia de E / S. O sistema também pode armazenar em cache o conteúdo do arquivo para tornar o processo do gato mais rápido.

    
por 17.09.2015 / 10:02
2

Se você ligar

i='cat input'

isso permite que seu processo de shell cresça em 50MB até 200MB (dependendo da implementação de caracteres internos). Isso pode tornar seu shell lento, mas esse não é o problema principal.

O principal problema é que o comando acima precisa ler o arquivo inteiro na memória do shell e o echo $i precisa fazer a divisão do campo no conteúdo do arquivo em $i . Para fazer a divisão de campo, todo o texto do arquivo precisa ser convertido em caracteres largos e é aí que a maior parte do tempo é gasto.

Eu fiz alguns testes com o caso lento e obtive estes resultados:

  • O mais rápido é o ksh93
  • A seguir está minha Bourne Shell (2x mais lenta que ksh93)
  • O próximo é o bash (3x mais lento que o ksh93)
  • O último é o ksh88 (7x mais lento que o 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 porque o tamanho da leitura tem alguma influência, eu compilei o Bourne Shell para ler em 4096 bytes em vez de 128 bytes e obtive o mesmo desempenho em ambos os casos.

    
por 17.09.2015 / 14:10
1

Em ambos os casos, o loop será executado apenas duas vezes (uma vez para a palavra seq e uma vez para a palavra 10 ).

Além disso, ambos irão mesclar espaços em branco adjacentes e deixar espaços em branco iniciais / finais, 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

Um motivo pelo qual o echo é mais lento pode ser que sua variável não citada esteja sendo dividida em espaço em branco em palavras separadas. Para 50MB isso será muito trabalho. Cite as variáveis!

Sugiro que corrija estes erros e reavalie os seus horários.

Eu testei isso localmente. Eu criei um arquivo de 50MB usando a saída de tar cf - | dd bs=1M count=50 . Eu também estendi os loops para serem executados por um fator de x100, para que os intervalos fossem dimensionados para um valor razoável (adicionei um loop adicional em torno 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 alguma coisa a versão contendo echo for executada ligeiramente mais rápida. Se eu remover as aspas e executar sua versão 2 quebrada, o tempo dobra, mostrando que o shell está tendo que fazer muito mais trabalho que deveria ser esperado.

time ./2original.sh

real    0m12.498s
user    0m8.645s
sys     0m2.732s
    
por 17.09.2015 / 08:26
-1

read é muito mais rápido que cat

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

cat demora 9,372 segundos. echo leva .232 segundos.

read é 40 vezes mais rápido .

Meu primeiro teste quando $p foi ecoado para a tela revelada read foi 48 vezes mais rápido que cat .

    
por 21.08.2018 / 03:00
-2

O echo destina-se a colocar 1 linha na tela. O que você faz no segundo exemplo é que você coloca o conteúdo do arquivo em uma variável e, em seguida, imprime essa variável. Na primeira você imediatamente coloca o conteúdo na tela.

cat é otimizado para esse uso. echo não é. Colocar 50Mb em uma variável de ambiente também não é uma boa ideia.

    
por 17.09.2015 / 08:27
-2

Não é sobre o eco ser mais rápido, é sobre o que você está fazendo:

Em um caso, você está lendo da entrada e da gravação para gerar a saída diretamente. Em outras palavras, o que for lido da entrada através do cat, vai para a saída através do stdout.

input -> output

No outro caso, você está lendo a entrada em uma variável na memória e, em seguida, 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 50MB.

    
por 17.09.2015 / 11:08