파일을 반복할 때 두 가지 방법이 있습니다:
-루프 사용
for
:for f in *; do echo "$f" done
사용
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 ...)이 더 빠르다는 것을 보여줍니다.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 * -prune
이전보다 속도가 느리면 쉘 이find . -maxdepth 1
glob을 처리한 다음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
확장 기능을 사용하면 이를 피할 수 있습니다 .-maxdepth
lstat
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
의 내장 버전이 있기 때문입니다 .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
다른 쉘보다 훨씬 느립니다).
답변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
가지 결함이 있으며 예상한 모든 파일을 찾지 못할 가능성이 높습니다.
- POSIX find는 여러 경로 매개변수를 허용한다고 보장되지 않습니다. 대부분의
find
구현에서는 이 작업을 수행하지만 여전히 이에 의존해서는 안 됩니다. find *
부딪히면 부서집니다ARG_MAX
.for f in *
아니요. 내장 기능이 아닌ARG_MAX
에 적용되기 때문입니다.exec
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