Bash에서 vs. 찾기 echo “$f” done 사용

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

  1. for루프를 사용하십시오 :

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

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

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



답변

1.

첫번째:

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

라는 파일에 실패 -n, -e그리고 같은 변종 -nene파일 이름은 백 슬래시를 포함하여, 일부 bash는 배포와 함께.

두번째:

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에서는 -maxdepth확장을 사용하여 피할 수 있습니다 lstat.

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

findstdio 출력 버퍼링을 제외하고 파일 이름을 찾 자마자 출력 하기 시작하므로 루프에서 수행하는 작업에 시간이 많이 걸리고 파일 이름 목록이 stdio 버퍼 이상인 경우 더 빠를 수 있습니다 (4 / 8 kB). 이 경우 루프 내 처리 find가 모든 파일 찾기를 마치기 전에 시작됩니다 . GNU 및 FreeBSD 시스템에서는 stdbuf더 빨리 발생 하도록 사용할 수 있습니다 (stdio 버퍼링 사용 안함).

5.

각 파일에 대해 명령을 실행하는 POSIX / standard / portable 방법 find-exec술어 를 사용하는 것입니다 .

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

그러나 echo쉘에는 내장 버전이 echo있지만 find새로운 프로세스를 생성하고 /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많은 느린 다른 조개에 비해 그입니다).


답변

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빨리 사용하는 것보다 (… I에 대해) find와 (출력을 배관 bash)

완벽 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 * -prunefind . -maxdepth 1전자 보다 속도가 느리면 쉘은 glob를 처리 한 다음에 (대형) 명령 줄을 작성 find합니다. NB : find . -prune그냥 반환합니다 ..

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

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

결론:

  • 지금까지 가장 느린 방법입니다. 이 접근법이 제안 된 답변에 대한 의견에서 지적했듯이 각 인수는 쉘을 생성합니다.

답변

찾기를 다음과 같이 바꾸더라도 찾기와 함께 확실히 갈 것입니다.

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

현명한 성능 find은 물론 필요에 따라 훨씬 빠릅니다. 현재 가지고 for있는 것은 현재 디렉토리의 파일 / 디렉토리 만 표시하지만 디렉토리 내용은 표시하지 않습니다. find를 사용하면 하위 디렉토리의 내용도 표시됩니다.

나는 당신 for*의지가 먼저 확장되어야 하기 때문에 찾기가 더 낫다고 말하고 파일의 양이 많은 디렉토리를 가지고 있다면 오류 인수 목록이 너무 길 것을 두려워합니다 . 동일find *

예를 들어, 현재 사용중인 시스템 중 하나에는 2 백만 개가 넘는 파일이있는 두 개의 디렉토리가 있습니다 (각 <100k).

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


답변

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

쓸모없는 사용 find-당신이 말하는 것은 효과적으로 “디렉토리 ( *) 의 각 파일에 대해 어떤 파일도 찾지 못합니다. 또한 여러 가지 이유로 안전하지 않습니다 :

  • 경로의 백 슬래시는 -r옵션 없이 특별히 처리 됩니다 read. 이것은 for루프 문제가 아닙니다 .
  • 경로의 줄 바꿈은 루프 내부의 사소한 기능을 손상시킵니다. 이것은 for루프 문제가 아닙니다 .

로 파일 이름을 처리하는 find것은 어렵 기 때문에 for가능할 때마다 루프 옵션을 사용해야합니다 . 또한 같은 외부 프로그램을 실행하면 find일반적으로 같은 내부 루프 명령을 실행하는 것보다 느립니다 for.


답변

그러나 우리는 성능 문제에 대한 빨판입니다! 이 실험 요청은별로 유효하지 않은 가정을 적어도 두 번 만듭니다.

A. 동일한 파일을 찾는다고 가정합니다.

글쎄, 그들은 처음에 같은 파일을 찾을 것입니다 . 왜냐하면 둘 다 같은 글 로프를 반복하기 때문 *입니다. 그러나 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


답변

find **경로가 아닌 술어처럼 보이는 토큰을 생성하면 올바르게 작동하지 않습니다 .

옵션의 끝을 나타내며 find의 옵션이 경로 앞에 --오므로 일반적인 인수를 사용하여이 문제를 해결할 수 없습니다 --.

이 문제를 해결하려면 find ./*대신 사용할 수 있습니다 . 그러나 정확히 같은 문자열을 생성하지는 않습니다 for x in *.

find ./* -prune | while read f ..사실의 스캔 기능을 사용하지 않습니다 find. ./*실제로 디렉토리를 탐색하고 이름을 생성하는 것은 글 로빙 구문 입니다. 그런 다음 find프로그램은 stat해당 이름 각각에 대해 최소한 확인 을 수행해야 합니다. 프로그램을 시작하고 이러한 파일에 액세스 한 다음 I / O를 수행하여 출력을 읽는 오버 헤드가 있습니다.

그것이보다 효율적일 수있는 방법을 상상하기는 어렵습니다 for x in ./* ....


답변

우선 forBash에 내장 된 쉘 키워드 find는 별개의 실행 파일입니다.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

for이 확장 할 때 루프 만 발견 된 어떤 디렉토리에 재귀하지 않습니다는 globstar 문자에서 파일을 찾을 수 있습니다.

반면에 찾기에는 globstar가 확장 한 목록이 제공되지만이 확장 목록 아래의 모든 파일과 디렉토리를 재귀 적으로 찾아서 각각 while루프로 파이프 합니다.

이 두 가지 접근 방식은 공백이 포함 된 경로 나 파일 이름을 처리하지 않는다는 점에서 위험한 것으로 간주 될 수 있습니다.

이것이이 두 가지 접근법에 대해 언급 할 가치가있는 모든 것입니다.