для против найти в Баш

28

При циклическом просмотре файлов существует два способа:

  1. используйте for-loop:

    for f in *; do
        echo "$f"
    done
  2. использовать find:

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

Предполагая, что эти два цикла найдут один и тот же список файлов, каковы различия между этими двумя параметрами в производительности и обработке?

rubo77
источник
1
Зачем? findне открывает файлы, которые он находит. Единственное, что я вижу, кусает вас здесь из-за большого количества файлов - это ARG_MAX .
Кодзиро
1
Смотрите ответы и комментарии, которые сообщают вам, что read fбудут искажать имена файлов при их чтении (например, имена с начальными пробелами). Также, find * -pruneкажется, очень запутанный способ сказать просто ls -1да?
Ян Д. Аллен
4
Не думайте, что два цикла найдут один и тот же набор файлов; в большинстве случаев они не будут. Кроме того, это должно быть find ., нет find *.
Алексис
1
@terdon Да, разбор ls -l- плохая идея. Но разбор ls -1(это 1не ан l) не хуже, чем разбор find * -prune. Оба не работают на файлах с символами новой строки в именах.
Ян Д. Аллен
5
Я подозреваю, что каждый из нас потратил больше времени на чтение этого вопроса и ответов, чем общая разница в производительности в течение срока действия рассматриваемого сценария.
mpez0

Ответы:

9

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.

3.

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

(выше с использованием -print0нестандартного расширения GNU / BSD ).

Это все еще включает в себя создание команды 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 / 8 кБ). В этом случае обработка внутри цикла начнется до того, как findбудет завершен поиск всех файлов. В системах GNU и FreeBSD вы можете использовать, stdbufчтобы это произошло раньше (отключение буферизации stdio).

5.

POSIX / стандартный / переносимый способ запуска команд для каждого файла с 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он намного медленнее, чем другие оболочки).

Стефан Шазелас
источник
что делает !команда find?
rubo77
@ rubo77, !для отрицания. ! -name . -prune more...будет делать -prunemore...так как -pruneвсегда возвращает true) для каждого файла, но .. Так что он будет работать more...со всеми файлами в ., но исключит .и не сойдет в подкаталоги .. Так что это стандартный эквивалент GNU -mindepth 1 -maxdepth 1.
Стефан Шазелас
18

Я попробовал это в каталоге с 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(потому что я в ...) быстрее, чем использовать 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- в первом случае оболочка обрабатывает глоб, а затем строит (большую) командную строку для find. NB: find . -pruneвозвращается просто ..

Больше тестов time find . -maxdepth 1 -exec echo {} \; >/dev/null:

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

Вывод:

  • самый медленный способ сделать это до сих пор. Как было указано в комментариях к ответу, где этот подход был предложен, каждый аргумент порождает оболочку.
Фил
источник
Какая труба избыточна? Вы можете показать линию, которую вы использовали без трубы?
rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; doneимеет избыточный канал - все, что делает канал, это findвыводит именно то, что выводит сам по себе. Без канала это было бы просто. find * -prune Канал только избыточен, потому что вещь на другой стороне канала просто копирует stdin в stdout (по большей части). Это дорогое бездействие. Если вы хотите что-то сделать с выводом find, кроме как просто выплюнуть его обратно, это другое дело.
Фил
Может быть, основное время занимает *. Как BitsOfNix сказал: я все еще настоятельно рекомендуем не использовать *и .для findвместо.
rubo77
@ rubo77 кажется таким. Я думаю, что я упустил это. Я добавил выводы для моей системы. Я предполагаю, find . -pruneчто быстрее, потому что findбудет читать дословно записи каталога, в то время как оболочка будет делать то же самое, потенциально сопоставляя с глобусом (может оптимизировать для *), а затем создавая большую командную строку для find.
Фил
1
find . -pruneпечатает только .в моей системе. Это почти не работает вообще. Это совсем не то, find * -pruneчто показывает все имена в текущем каталоге. Голые read fбудут искажать имена файлов с начальными пробелами.
Ян Д. Аллен
10

Я бы определенно пошел с поиском, хотя я бы изменил вашу находку на это

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

Производительность мудрая, findнамного быстрее, в зависимости от ваших потребностей, конечно. То, что у вас сейчас forесть, будет отображать только файлы / каталоги в текущем каталоге, но не содержимое каталогов. Если вы используете find, он также покажет содержимое подкаталогов.

Я говорю находкой лучше , так как с вашим должны быть расширены первым , и я боюсь , что если у вас есть каталог с огромным количеством файлов , которые он может дать ошибку список аргументов слишком долго . То же самое касаетсяfor*find *

Например, в одной из систем, которые я сейчас использую, есть пара каталогов с более чем 2 миллионами файлов (<100k каждая):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
источник
Я добавил, -pruneчтобы сделать два примера более похожими. и я предпочитаю трубу с помощью while, чтобы было проще применять больше команд в цикле
rubo77 22.10.13
изменение жесткого предела вряд ли является подходящим решением для моего POV. Особенно, когда речь идет о 2 с лишним миллионах файлов. Без отступления от Вопроса, для простых случаев, поскольку каталог на одном уровне быстрее, но если вы измените структуру вашего файла / каталога, будет сложнее мигрировать. Хотя с find и огромным количеством опций вы можете быть лучше подготовлены. Тем не менее я все еще настоятельно рекомендую не использовать * и. вместо найти. Это было бы более переносимо, чем *, где вы не сможете контролировать hardlimit ...
BitsOfNix 22.10.13
4
Это будет порождать один эхо-процесс на файл (в то время как в оболочке для цикла это встроенная функция эха, которая будет использоваться без разветвления дополнительного процесса), и будет переходить в каталоги, так что это будет намного медленнее . Также обратите внимание, что он будет включать точечные файлы.
Стефан Шазелас
Вы правы, я добавил maxdepth 1, чтобы он придерживался только текущего уровня.
BitsOfNix
7
find * -prune | while read f; do 
    echo "$f"
done

это бесполезное использование find- «То, что вы говорите, эффективно» для каждого файла в каталоге ( *), не находите никаких файлов. Кроме того, это небезопасно по нескольким причинам:

  • Обратные слеши в путях обрабатываются специально без -rопции read. Это не проблема с forциклом.
  • Новые строки в путях нарушат любую нетривиальную функциональность внутри цикла. Это не проблема с forциклом.

Обработка любого имени файла с findэто трудно , так что вы должны использовать forопцию цикла всякий раз , когда это возможно только по этой причине. Кроме того, запуск такой внешней программы, как findправило, будет выполняться медленнее, чем выполнение внутренней команды цикла for.

l0b0
источник
@ I0b0 А как насчет find -path './*' -prune или find -path './[^.]*' -prune (чтобы избежать скрытых файлов и каталогов) в качестве лучшей конструкции - в полной форме: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Ни find's', -print0ни xargs'' ' -0не совместимы с POSIX, и вы не можете помещать произвольные команды sh -c ' ... '(одинарные кавычки не могут быть экранированы внутри одинарных кавычек), так что это не так просто.
10
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
Кодзиро
источник
Я не согласен с утверждением, что цикл while порождает подоболочку - в худшем случае - новый поток: следующее пытается показать до и после извинения за плохое форматирование$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Фил
Технически я ошибаюсь: канал вызывает неявную подоболочку, а не цикл while. Я отредактирую
Кодзиро
2

find * не будет работать правильно, если * создает токены, которые выглядят как предикаты, а не пути.

Вы не можете использовать обычный -- аргумент, чтобы исправить это, потому что --указывает конец параметров, а параметры поиска идут перед путями.

Чтобы исправить эту проблему, вы можете использовать find ./*вместо этого. Но тогда он не производит точно такие же строки, какfor x in * .

Обратите внимание, что на find ./* -prune | while read f ..самом деле не использует функцию сканирования find. Это глобальный синтаксис, ./*который фактически пересекает каталог и генерирует имена. Тогда findпрограмма должна будет выполнить хотя быstat проверку каждого из этих имен. У вас есть накладные расходы, связанные с запуском программы и доступом к этим файлам, а затем вводом-выводом для чтения ее вывода.

Трудно представить, как это может быть чем-то менее эффективным, чем for x in ./* ....

Kaz
источник
1

Начнем forс того, что ключевое слово shell, встроенное в Bash, findявляется отдельным исполняемым файлом.

$ type -a for
for is a shell keyword

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

forЦикл будет найти только файлы из globstar характера , когда она расширяется, она не будет рекурсией в любые каталоги , которые он находит.

Находить с другой стороны также будет предоставлен список, расширенный globstar, но он рекурсивно найдет все файлы и каталоги под этим расширенным списком и направит каждый из них к while цикл.

Оба этих подхода могут считаться опасными в том смысле, что они не обрабатывают пути или имена файлов, содержащие пробелы.

Это все, что я могу придумать, чтобы прокомментировать эти 2 подхода.

SLM
источник
Я добавил -prune к команде поиска, чтобы они были более похожи.
rubo77
0

Если все файлы, возвращаемые функцией find, могут быть обработаны с помощью одной команды (очевидно, неприменимо к вашему эхо-примеру выше), вы можете использовать xargs:

find * |xargs some-command
обкрадывать
источник
0

В течение многих лет я использовал это: -

find . -name 'filename'|xargs grep 'pattern'|more

искать определенные файлы (например, * .txt), которые содержат шаблон, который может искать grep, и направлять его в большее, чтобы он не скроллировал с экрана. Иногда я использую канал >>, чтобы записать результаты в другой файл, который я смогу просмотреть позже.

Вот пример результата:

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
Аллен
источник