Bash에서 찾기

Bash에서 찾기

파일을 반복할 때 두 가지 방법이 있습니다:

  1. -루프 사용 for:

    for f in *; do
        echo "$f"
    done
    
  2. 사용 find:

    find * -prune | while read f; do 
        echo "$f"
    done
    

두 루프가 동일한 파일 목록을 찾을 것이라고 가정하면 두 옵션의 차이점은 무엇입니까성능그리고 처리?

답변1

나는 2259개의 항목이 있는 디렉토리에서 이것을 시도하고 time명령을 사용했습니다.

출력 time for f in *; do echo "$f"; done(파일 제외!)은 다음과 같습니다.

real    0m0.062s
user    0m0.036s
sys     0m0.012s

출력 time find * -prune | while read f; do echo "$f"; done(파일 제외!)은 다음과 같습니다.

real    0m0.131s
user    0m0.056s
sys     0m0.060s

캐시 누락을 제거하기 위해 각 명령을 여러 번 실행합니다. 이는 출력을 사용하고 파이핑하는 것(에 대해 ) 을 유지하는 것 bash(for i in ...)이 더 빠르다는 것을 보여줍니다.findbash

완벽함을 위해 find귀하의 예에서는 파이프가 완전히 중복되므로 파이프를 제거했습니다. 그냥 출력은 다음 find * -prune과 같습니다

real    0m0.053s
user    0m0.016s
sys     0m0.024s

또한 time echo *(출력은 개행으로 구분되지 않습니다.)

real    0m0.009s
user    0m0.008s
sys     0m0.000s

echo *이 시점에서 더 빠른 이유는 줄 바꿈을 많이 출력하지 않아서 출력이 많이 스크롤되지 않기 때문이라고 생각합니다 . 테스트해보자...

time find * -prune | while read f; do echo "$f"; done > /dev/null

생산하다:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

그리고 time find * -prune > /dev/null출력은 다음과 같습니다.

real    0m0.027s
user    0m0.008s
sys     0m0.012s

그리고 time for f in *; do echo "$f"; done > /dev/null다음을 생산합니다:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

마지막으로: time echo * > /dev/null수율:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

일부 변동은 무작위 요인으로 설명될 수 있지만 이는 분명해 보입니다.

  • 출력 속도가 느림
  • 파이프라인 비용이 약간 있음
  • for f in *; do ...find * -prune자체보다 느리지만 파이프와 관련된 위 구조물의 경우 더 빠릅니다.

또한, 그런데 두 방법 모두 공백이 있는 이름을 잘 처리하는 것 같습니다.

편집하다:

find . -maxdepth 1 > /dev/null시간이 지남에 따라 find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

따라서 추가 결론은 다음과 같습니다.

  • find * -prune이전보다 속도가 느리면 쉘 이 find . -maxdepth 1glob을 처리한 다음 find.find . -prune.

더 많은 테스트 time find . -maxdepth 1 -exec echo {} \; >/dev/null::

real    0m3.389s
user    0m0.040s
sys     0m0.412s

결론적으로:

  • 지금까지 가장 느린 방법입니다. 이 접근 방식을 제안하는 답변의 설명에서 지적했듯이 각 인수에 대해 쉘이 생성됩니다.

답변2

1.

첫 번째:

for f in *; do
  echo "$f"
done

-n-e파일 이름에 백슬래시가 포함된 일부 bash 배포에서는 이름이 및 및 변형인 파일에 대해 -nene실패합니다 .

두번째:

find * -prune | while read f; do 
  echo "$f"
done

더 많은 경우에 실패합니다( !, -H, -name, (, 이름이 공백으로 시작하거나 끝나거나 개행 문자를 포함하는 파일 이름...).

인수로 받은 파일을 인쇄하는 것 외에는 아무 작업도 수행하지 않는 *확장 쉘입니다 . 대신 find내장을 사용 하거나 피할 수도 있습니다printf '%s\n'printf매개변수가 너무 많습니다.잠재적인 오류.

2.

확장은 *정렬되며 정렬이 필요하지 않은 경우 속도가 빨라질 수 있습니다. 존재하다 zsh:

for f (*(oN)) printf '%s\n' $f

또는 간단하게:

printf '%s\n' *(oN)

bash내가 아는 한 이에 상응하는 것이 없으므로 에 의존해야 합니다 find.

삼.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(위에서는 GNU/BSD -print0비표준 확장을 사용합니다).

여기에는 여전히 find 명령 생성 및 느린 루프 사용이 포함되므로 파일 목록이 매우 크지 않는 한 루프를 사용하는 것보다 속도가 느릴 while read수 있습니다 .for

4.

또한 쉘 와일드카드 확장과 달리 각 파일에 대해 시스템 호출을 수행 find하므로 lstat비정렬로는 이를 보상할 가능성이 없습니다.

GNU/BSD의 경우 최적화 저장을 실행하는 find확장 기능을 사용하면 이를 피할 수 있습니다 .-maxdepthlstat

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

파일 이름의 출력은 발견되자마자 시작되기 때문에 find(stdio 출력 버퍼 제외) 루프에서 수행하는 작업이 시간이 많이 걸리고 파일 이름 목록이 stdio 버퍼(4/8kB)보다 큰 경우. 이 경우 루프 내 처리는 find모든 파일 검색이 완료되기 전에 시작됩니다. GNU 및 FreeBSD 시스템에서는 stdbuf이를 사용하여 더 빠르게 수행할 수 있습니다(stdio 버퍼링 비활성화).

5.

각 파일에 대해 명령을 실행하는 POSIX/표준/이식 가능한 방법은 조건자를 find사용하는 것입니다 .-exec

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

하지만 이 경우에는 셸에서 반복하는 것보다 덜 효율적입니다. 셸 에는 새 프로세스를 생성하고 각 파일에 대해 실행 해야 하는 while echo의 내장 버전이 있기 때문입니다 .echofind/bin/echo

여러 명령을 실행해야 하는 경우 다음을 수행할 수 있습니다.

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

cmd2하지만 성공할 경우에만 실행된다는 점에 유의하세요 .cmd1

6.

각 파일에 대해 복잡한 명령을 실행하는 표준 방법은 다음을 사용하여 셸을 호출하는 것입니다 -exec ... {} +.

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

이 시점에서는 기본 제공 버전을 echo사용 하고 가능한 한 적은 수의 버전을 생성하므로 효율성이 다시 향상됩니다 .sh-exec +sh

7.

존재하다200,000개의 파일이 있는 디렉토리에서 테스트 중입니다.ext4의 짧은 이름의 경우 첫 번째 zsh항목(2항)이 가장 빠르며 첫 번째 간단한 for i in *루프가 그 뒤를 따릅니다(비록 평소와 같이 bash다른 쉘보다 훨씬 느립니다).

답변3

나는 확실히 찾기를 선택하겠지만, 찾기를 다음과 같이 변경하겠습니다.

find . -maxdepth 1 -exec echo {} \;

물론 성능 측면에서는 find필요에 따라 훨씬 더 빠릅니다. 현재 가지고 있는 것은 for디렉터리 내용이 아닌 현재 디렉터리의 파일/디렉터리만 표시합니다. find를 사용하면 하위 디렉토리의 내용도 표시됩니다.

for먼저 확장 해야 하기 때문에 find가 더 낫다고 말하고 *, 파일이 많은 디렉토리가 있으면 오류가 발생할까 걱정됩니다.매개변수 목록이 너무 깁니다.. 다음에도 적용됩니다.find *

예를 들어, 현재 작업 중인 시스템에는 2백만 개가 넘는 파일(각각 10만 개 미만)이 포함된 여러 디렉터리가 있습니다.

find *
-bash: /usr/bin/find: Argument list too long

답변4

하지만 우리는 성능 문제에 집착하고 있습니다! 실험에 대한 이 요청은 유효성을 떨어뜨리는 최소한 두 가지 가정을 합니다.

A. 동일한 파일을 찾았다고 가정하면...

글쎄, 그들은~ 할 것이다동일한 파일이 모두 동일한 glob을 반복하기 때문에 먼저 발견됩니다. 즉, *find * -prune | while read f가지 결함이 있으며 예상한 모든 파일을 찾지 못할 가능성이 높습니다.

  1. POSIX find는 여러 경로 매개변수를 허용한다고 보장되지 않습니다. 대부분의 find구현에서는 이 작업을 수행하지만 여전히 이에 의존해서는 안 됩니다.
  2. find *부딪히면 부서집니다 ARG_MAX. for f in *아니요. 내장 기능이 아닌 ARG_MAX에 적용되기 때문입니다.exec
  3. while read f공백으로 시작하고 끝나는 파일 이름을 분리할 수 있으며 공백은 제거됩니다. while read기본 매개변수를 사용하여 이 문제를 극복 할 수 있지만 REPLY파일 이름에 줄바꿈이 포함된 경우에는 여전히 도움이 되지 않습니다.

B.. echo아무도 단지 파일 이름을 에코하기 위해 이 작업을 수행하지는 않습니다. 이 작업을 수행하려면 다음 중 하나를 수행하세요.

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

여기서 루프된 파이프는 while루프 끝에서 닫히는 암시적 하위 쉘을 생성하는데, 이는 일부 사람들에게는 직관적이지 않을 수 있습니다.

이 질문에 답하기 위해 184개의 파일과 디렉터리가 포함된 내 디렉터리의 결과는 다음과 같습니다.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

관련 정보