Por que o eco é muito mais rápido que o toque?

116

Estou tentando atualizar o registro de data e hora para o horário atual em todos os arquivos xml no meu diretório (recursivamente). Estou usando o Mac OSX 10.8.5.

Em cerca de 300.000 arquivos, o seguinte echocomando leva 10 segundos :

for file in `find . -name "*.xml"`; do echo >> $file; done

No entanto, o touchcomando a seguir leva 10 minutos ! :

for file in `find . -name "*.xml"`; do touch $file; done

Por que o eco é muito mais rápido do que o toque aqui?

polym
fonte
20
Apenas uma observação lateral: Você não sabe que esses dois comandos não são equivalentes, não é? Pelo menos para Unix / Linux, o echo >> $filearquivo anexará uma nova linha $filee, portanto, a modificará. Presumo que será o mesmo para o OS / X. Se você não quiser isso, use echo -n >> $file.
Dubu
2
Também não touch `find . -name "*.xml"` seria ainda mais rápido do que os dois itens acima?
elmo
4
Ou considerar apenas>>$file
gerrit
8
Não é uma resposta para a pergunta explícita, mas por que invocar touchtantas vezes? find . -name '*.xml' -print0 | xargs -0 touchinvoca touchmuito menos vezes (possivelmente apenas uma vez). Funciona em Linux, deve trabalhar no OS X.
Mike Renfro
3
@elmo lista de argumentos muito longa (facilmente, com 300.000 arquivos ...)
Rmano

Respostas:

161

No bash, touché um binário externo, mas echoé um shell embutido :

$ type echo
echo is a shell builtin
$ type touch
touch is /usr/bin/touch

Como touché um binário externo e você invoca touchuma vez por arquivo, o shell deve criar 300.000 instâncias touch, o que leva muito tempo.

echo, no entanto, é um shell embutido e a execução de shell embutidos não requer bifurcação. Em vez disso, o shell atual executa todas as operações e nenhum processo externo é criado; esta é a razão pela qual é muito mais rápido.

Aqui estão dois perfis das operações do shell. Você pode ver que é gasto muito tempo clonando novos processos ao usá-lo touch. Usar em /bin/echovez do shell embutido deve mostrar um resultado muito mais comparável.


Usando o toque

$ strace -c -- bash -c 'for file in a{1..10000}; do touch "$file"; done'
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 56.20    0.030925           2     20000     10000 wait4
 38.12    0.020972           2     10000           clone
  4.67    0.002569           0     80006           rt_sigprocmask
  0.71    0.000388           0     20008           rt_sigaction
  0.27    0.000150           0     10000           rt_sigreturn
[...]

Usando eco

$ strace -c -- bash -c 'for file in b{1..10000}; do echo >> "$file"; done'
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 34.32    0.000685           0     50000           fcntl
 22.14    0.000442           0     10000           write
 19.59    0.000391           0     10011           open
 14.58    0.000291           0     20000           dup2
  8.37    0.000167           0     20013           close
[...]
Chris Down
fonte
1
Você compilou strace no OS X ou executou seu teste em outro sistema operacional?
bmike
1
@bmike Meu teste é no Linux, mas o princípio é idêntico.
Chris Baixo
Eu concordo totalmente - veja meu comentário sobre a pergunta principal sobre como / bin / echo é tão lento quanto / bin / touch, para que o raciocínio seja correto. Eu só queria reproduzir o tempo do strace e falhei usando o dtruss / dtrace e a sintaxe bash -c também não funciona como esperado no OS X.
bmike
71

Como outros responderam, o uso echoserá mais rápido do que o touchque echoé um comando que é comumente (embora não seja necessário) embutido no shell. Seu uso dispensa a sobrecarga do kernel associada à execução de um novo processo para cada arquivo que você obtém touch.

No entanto, observe que a maneira mais rápida de obter esse efeito ainda está em uso touch, mas, em vez de executar o programa uma vez para cada arquivo, é possível usar a -execopção com findpara garantir que seja executada apenas algumas vezes. Essa abordagem geralmente será mais rápida, pois evita a sobrecarga associada a um loop de shell:

find . -name "*.xml" -exec touch {} +

O uso de +(em oposição a \;) com find ... -execexecuta o comando apenas uma vez, se possível, com cada arquivo como argumento. Se a lista de argumentos for muito longa (como é o caso de 300.000 arquivos), várias execuções serão feitas com uma lista de argumentos com um comprimento próximo ao limite ( ARG_MAXna maioria dos sistemas).

Outra vantagem dessa abordagem é que ela se comporta de maneira robusta com nomes de arquivos que contêm todos os caracteres de espaço em branco, o que não é o caso do loop original.

Graeme
fonte
17
+1por apontar o +argumento find . Eu acho que muitas pessoas não estão cientes disso (eu não estava).
gerrit
7
Nem todas as versões de findtêm o +argumento. Você pode obter um efeito semelhante canalizando para xargs.
Barmar
5
@ Barmar, a +peça é requerida pelo POSIX, portanto deve ser portátil. -print0não é.
Graeme
1
Ocasionalmente ainda deparo com implementações que não a possuem. YMMV.
Barmar
1
@ ChrisDown, algo que eu descobri é que o Busybox findtem a opção disponível, mas apenas a trata como uma ;parte inferior da superfície.
Graeme
29

echoé um shell embutido. Por outro lado, touché um binário externo.

$ type echo
echo is a shell builtin
$ type touch
touch is hashed (/usr/bin/touch)

Os componentes internos do shell são muito mais rápidos, pois não há sobrecarga envolvida no carregamento do programa, ou seja, não há fork/ execenvolvido. Como tal, você observaria uma diferença de tempo significativa ao executar um comando interno versus um comando externo várias vezes.

Esta é a razão pela qual utilitários como timeestão disponíveis como componentes internos do shell.

Você pode obter a lista completa dos componentes internos do shell, dizendo:

enable -p

Como mencionado acima, o uso do utilitário em oposição ao interno resulta em uma degradação significativa do desempenho. A seguir, estão as estatísticas do tempo necessário para criar ~ 9000 arquivos usando o builtin echo e o utilitário echo :

# Using builtin
$ time bash -c 'for i in {1000..9999}; do echo > $i; done'

real    0m0.283s
user    0m0.100s
sys 0m0.184s

# Using utility /bin/echo
$ time bash -c 'for i in {1000..9999}; do /bin/echo > $i; done'

real    0m8.683s
user    0m0.360s
sys 0m1.428s
devnull
fonte
E eu acho que há um echobinário na maioria dos sistemas (para mim é /bin/echo), então você pode repetir os testes de controle de tempo usando que em vez do built-in
Michael Mrozek
@MichaelMrozek Adicionados testes de tempo para o builtin e o binário.
devnull