Por que a combinação grep / -r / - include é mais lenta que a combinação find / -exec / grep?

6

Pelo que entendi, os dois comandos seguintes aproximadamente realizam a mesma coisa:

Comando 1:

find -name "filename.xml" -exec grep someString {} \;

Comando 2:

grep -r --include=filename.xml someString .
Ainda assim, ao cronometrá-los após o aquecimento no mesmo contexto, o primeiro foi cerca de 3 vezes mais rápido que o segundo (algo como 4 segundos versus 12 segundos).

O número de arquivos que correspondem ao padrão do nome do arquivo na árvore de pastas que testei era muito pequeno, e cada um desses arquivos era muito pequeno. Isso me faz pensar que a maior parte do tempo foi gasto em encontrar os arquivos correspondentes ao padrão do nome do arquivo, e não no grepping desses arquivos correspondentes.

Então, por que existe uma diferença tão grande no desempenho dessas duas linhas de comando?

    
por killy971 18.01.2013 / 03:49

2 respostas

3

Na verdade, é o contrário. o comando grep tende a ser mais eficiente em geral.

Vou trabalhar em um instantâneo da árvore do Portage do Gentoo, que está disponível publicamente se você quiser experimentar.

 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null

real    0m1.184s
user    0m0.033s
sys     0m0.130s

 $ time grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null

real    0m0.017s
user    0m0.007s
sys     0m0.010s

Vamos ver quais funções são mais chamadas para cada uma:

 $ (strace find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   3574 fcntl
   1597 close
    794 newfstatat
    794 getdents
    689 wait4
    689 clone
    689 --- SIGCHLD 
    404 fstat
    397 openat
     20 mmap

 $ (strace grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   2779 fcntl
   1493 close
   1382 read
   1096 fstat
   1087 openat
    794 getdents
    792 newfstatat
    691 ioctl
    689 lseek
     25 write

Veja também as chamadas que foram longas:

 $ (strace -T find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/\(.*\)<\(.*\)>/ /g' | sort -nk1r | head -n10
exit_group(0)                           = ?
0.001884 wait4(29725, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29725 
0.001879 wait4(29475, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29475 
0.001813 wait4(29430, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29430 
0.001812 wait4(30089, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30089 
0.001807 wait4(29722, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29722 
0.001795 wait4(29645, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29645 
0.001794 wait4(29848, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29848 
0.001759 wait4(30032, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30032 
0.001754 wait4(30093, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30093

 $ (strace -T grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& 
exit_group(0)                           = ?
0.002336 fcntl(3, F_SETFD, FD_CLOEXEC)           = 0 
0.000460 read(3, "7ELF
 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} + > /dev/null

real    0m0.027s
user    0m0.010s
sys     0m0.013s
 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null

real    0m1.184s
user    0m0.033s
sys     0m0.130s

 $ time grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null

real    0m0.017s
user    0m0.007s
sys     0m0.010s
 $ (strace find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   3574 fcntl
   1597 close
    794 newfstatat
    794 getdents
    689 wait4
    689 clone
    689 --- SIGCHLD 
    404 fstat
    397 openat
     20 mmap

 $ (strace grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& sed 's/[({].*//g' | sort | uniq -c | sort -r | head -n 10
   2779 fcntl
   1493 close
   1382 read
   1096 fstat
   1087 openat
    794 getdents
    792 newfstatat
    691 ioctl
    689 lseek
     25 write
 $ (strace -T find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} \; > /dev/null) |& sed 's/\(.*\)<\(.*\)>/ /g' | sort -nk1r | head -n10
exit_group(0)                           = ?
0.001884 wait4(29725, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29725 
0.001879 wait4(29475, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29475 
0.001813 wait4(29430, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29430 
0.001812 wait4(30089, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30089 
0.001807 wait4(29722, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29722 
0.001795 wait4(29645, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29645 
0.001794 wait4(29848, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 29848 
0.001759 wait4(30032, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30032 
0.001754 wait4(30093, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 30093

 $ (strace -T grep -r --include '*.ebuild' DEPEND /usr/portage/sys-apps/ > /dev/null) |& 
exit_group(0)                           = ?
0.002336 fcntl(3, F_SETFD, FD_CLOEXEC)           = 0 
0.000460 read(3, "7ELF
 $ time find /usr/portage/sys-apps/ -name '*.ebuild' -exec grep DEPEND {} + > /dev/null

real    0m0.027s
user    0m0.010s
sys     0m0.013s
%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%>%pre%%pre%%pre%%pre%0'C6%pre%%pre%%pre%"..., 832) = 832 0.000313 close(3) = 0 0.000295 execve("/bin/grep", ["grep", "-r", "--include", "*.ebuild", "DEPEND", "/usr/portage/sys-apps/"], [/* 75 vars */]) = 0 0.000276 fcntl(3, F_SETFD, FD_CLOEXEC) = 0 0.000265 getdents(3, /* 244 entries */, 32768) = 7856 0.000233 fstat(3, {st_mode=S_IFREG|0644, st_size=826, ...}) = 0 0.000162 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 0.000137 lseek(3, 1402, 0x4 /* SEEK_??? */) = -1 ENXIO (No such device or address)
%pre%%pre%%pre%%pre%%pre%%pre%>%pre%%pre%%pre%%pre%0'C6%pre%%pre%%pre%"..., 832) = 832 0.000313 close(3) = 0 0.000295 execve("/bin/grep", ["grep", "-r", "--include", "*.ebuild", "DEPEND", "/usr/portage/sys-apps/"], [/* 75 vars */]) = 0 0.000276 fcntl(3, F_SETFD, FD_CLOEXEC) = 0 0.000265 getdents(3, /* 244 entries */, 32768) = 7856 0.000233 fstat(3, {st_mode=S_IFREG|0644, st_size=826, ...}) = 0 0.000162 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 0.000137 lseek(3, 1402, 0x4 /* SEEK_??? */) = -1 ENXIO (No such device or address)

Bastante interessante, você vê nesta duração que a saída está esperando muito, enquanto o grep faz algumas coisas necessárias para iniciar e parar o processo. As chamadas de espera levam mais de 0,001s, enquanto as chamadas de busca diminuem para um nível constante de 0,0002s.

Se você olhar para as chamadas wait4 na saída da contagem, você notará que há uma quantidade igual de chamadas clonadas e sinais SIGCHLD ocorrendo; isso ocorre porque o find chama o processo do grep para cada arquivo que encontra , e é aí que sua eficiência sofre, já que a clonagem e a espera são caras.

Há ocasiões em que não sofre ; você pode obter um conjunto de arquivos pequeno o suficiente para não haver muita sobrecarga de iniciar vários processos do grep, você também pode ter um disco muito lento que negligencie a sobrecarga de iniciar um novo processo, e provavelmente há outras razões também. No entanto, ao comparar a velocidade, muitas vezes observamos o quão bem uma ou outra abordagem é dimensionada e não analisamos casos especiais de canto.

No seu caso, você mencionou que " É por isso que eu sinto que é a maneira como o" grep "visita a árvore de diretórios que é ineficiente em comparação com" find ". " caso; como você pode ver, 1382 chamadas de leitura foram feitas, enquanto o find não faz isso, isso torna o grep mais E / S intensivo para você.

TL; DR: Para ver por que os intervalos são ineficientes, tente fazer essa análise novamente e identifique o problema no seu caso, para saber por que seus dados específicos e tarefas não são eficientes no grep; você descobrirá como o grep diferente pode se comportar no seu caso de canto ...

Assim, como outros sugeriram, você deve certificar-se de que ele não chame grep para cada arquivo, o que pode ser feito substituindo \; por + no final.

%pre%

Como você pode ver, 0.027s chega bem perto de 0.017s; a diferença é principalmente atribuída ao fato de que ele ainda tem que chamar tanto o find quanto o grep ao invés de apenas grep sozinho. Ou, como mostrado nos comentários, em alguns sistemas, o + permite melhorar o grep.

    
por 16.03.2013 / 09:05
3

Provavelmente é melhor usar um Wakizashi e não um Katana para descascar batatas, mas também não é uma boa ferramenta para o trabalho. O mesmo se aplica às ferramentas digitais, use-as com sabedoria.

Isto pode soar como uma sugestão vazia, mas neste caso, por exemplo, o grep é executado uma vez para cada arquivo no exemplo de busca. Isso não é sábio desempenho sábio. Se você substituir o argumento de fechamento de find por '+', ao contrário de '\;', o grep será executado apenas uma vez para todos os arquivos encontrados.

Para responder com certeza em um caso como este, seria necessário comparar as partes relevantes do código-fonte para o grep e localizar para ver qual é mais rápido na correspondência (localização) dos nomes dos arquivos. Francamente, isso está além das minhas habilidades.

Intuitivamente, eu diria que o find é otimizado para procurar por arquivos em diretórios, enquanto o grep é otimizado para procurar por strings em arquivos. Além disso, a opção --include deve funcionar com arquivos em maiúsculas e minúsculas, enquanto o '-name

edit: (minhas descobertas estavam erradas)

Algumas investigações básicas em uma pasta doc com ~ 35 mil arquivos de arquivos:

$ strace find . -name "moo" -exec grep a {} \+ 2>&1 |grep ^open |wc -l
4448

$ strace grep -r --include=moo  a . 2>&1 | grep ^open | wc -l
2289

A combinação de localização abre muito mais arquivos. Isso sugere o oposto de suas descobertas. Eu fiz algum timing básico (como Tom Wijsman).

DIR=imagemagick-6.7.8.7
$ findhtml $DIR |& top10    $ grephtml $DIR |& top10
  1617 mmap2                  316 read
  1176 fstat64                173 close
  1176 close                  164 fstat64
   735 open                   157 openat
   608 read                   148 ioctl
   588 mprotect                63 fcntl64
   441 brk                     25 getdents64
   294 munmap                  16 fstatat64
   294 ioctl                   11 mmap2
   147 write                    5 write
time: Real 0m2.0s           time: Real 0m0.3s

Descobri que o strace find aponta para / usr / lib / locale / locale-archive, mas não tenho certeza de quais são as implicações.

    
por 03.03.2013 / 09:08