Я работаю над проектом Python, где мне нужно искать определенные файлы в очень большой структуре каталогов. В настоящее время я использую метод glob (или rglob) из модуля pathlib, но он довольно медленный из-за большого количества файлов и каталогов.
Вот упрощенная версия моего текущего кода:
from pathlib import Path
base_dir = Path("/path/to/large/directory")
files = list(base_dir.rglob("ind_stat.zpkl"))
Это работает, но слишком медленно, поскольку приходится проходить через огромное количество каталогов и файлов. В идеале я хотел бы разделить работу по обходу каталога между несколькими потоками или процессами, чтобы повысить производительность. Существуют ли оптимизации или альтернативные библиотеки/методы, которые могут помочь повысить производительность?
Другой, более быстрый и простой способ, если вы знаете, что /path/to/large/directory
не будет меняться очень часто, — это выполнить обход только один раз и сохранить список файлов, например. базу данных SQLite и выполните запрос к ней.
обратите внимание, что параллельный поиск файлов имеет смысл только в медленной сетевой файловой системе, которая допускает одновременные запросы. Но, скорее всего, для этого есть более эффективный API. При поиске на локальной ФС простой rglob
будет быстрее практически независимо от типа хранилища. Массовый параллелизм без сравнительного анализа слишком часто является пессимизацией.
Вы не упоминаете свою файловую систему, тип/конфигурацию диска, объем данных, количество файлов, глубину/расположение файлов или текущую/желаемую производительность...
@LogicDaemon «При поиске в локальной файловой системе простой rglob будет быстрее почти независимо от типа хранилища» — на чем вы это основываете? Быстрее, чем что? Учитывая, что все биты, включая подстановку, используемые rglob
, написаны на Python, вероятно, можно легко придумать более быструю и менее общую функцию для поиска всех ind_stat.zpkl
.
@AKX, который в основном основан на двух предположениях: 1. Интерфейс между системой хранения и оперативной памятью является последовательным (SATA или PCIe или что-то еще). 2. Процессор достаточно быстр, чтобы обрабатывать все операции ввода-вывода с помощью одного ядра. Второе предположение не всегда верно, но в тех случаях, когда оно неверно, накладные расходы на многопроцессорную обработку также гораздо более значительны. Упомянутый вами rglob
на самом деле не выполняет никакой тяжелой обработки в коде Python. Он использует os.scandir
под капотом, что довольно быстро. Также см. Softwareengineering.stackexchange.com/q/408687/436501
@LogicDaemon Если rglob
не выполняет тяжелую обработку, забавно, как можно реализовать задачу OP в 1,8 раза быстрее, чем rglob
тогда (см. мой ответ)! Во-вторых, поскольку на самом деле это не считывает фактические (большие) данные с диска, я предполагаю, что в ОС уже кэшировано множество структур FS для каталогов и т. д., поэтому накладные расходы системы хранения не будут иметь большого значения. там.
@AKX мой первоначальный комментарий был о параллельном поиске, о котором говорилось в вопросе. И как показывает пример SIGHUP, это хуже вашего варианта. Для меня до сих пор сюрприз, что rglob
так заметно тормозит, спасибо, что открыли мне глаза :)
@LogicDaemon Я все еще верю, что ядро может обслуживать несколько вызовов opendir и getdirentries для разных пользовательских потоков параллельно. Потоки Python, конечно, совсем другие из-за GIL.
@AKX Я согласен, что ядро делает это. Но накладные расходы, связанные с созданием процесса для работы с GIL и последующей синхронизацией, почти никогда не окупаются в cpython для операций с локальными каталогами (оно того стоит для относительно больших файлов).
Вы также можете удалить свой вопрос, если вы не собираетесь отвечать на запросы о разъяснениях или голосовать за / принимать любые ответы, которые любезно предложили люди.
Эмпирически на моем Mac в дереве каталогов, содержащем 73 429 файлов и 3315 каталогов (и 3 файла с именем Snap.wav):
~/Samples $ pwd
/Users/akx/Samples
~/Samples $ find . -type f | wc -l
73429
~/Samples $ find . -type d | wc -l
3315
тривиальная реализация на основе os.walk()
в 1,8 раза быстрее, чем rglob
:
import os
import pathlib
base_path = pathlib.Path("/Users/akx/Samples")
file_to_find = "Snap.wav"
def find_snaps_rglob():
paths = []
for path in base_path.rglob(file_to_find):
paths.append(path)
return paths
def find_snaps_walk():
paths = []
for dp, dn, fn in os.walk(base_path):
for f in fn:
if f == file_to_find:
paths.append(pathlib.Path(os.path.join(dp, f)))
return paths
assert sorted(find_snaps_rglob()) == sorted(find_snaps_walk())
name='find_snaps_rglob' iters=10 time=1.622 iters_per_sec=6.17
name='find_snaps_walk' iters=20 time=1.740 iters_per_sec=11.49
Реализация ходьбы со стеком и os.scandir()
еще немного ускоряет работу (iters_per_sec=12.98
), но для этого, вероятно, не потребуется, например, символические ссылки и ошибки, например walk
или rglob
, могут быть учтены.
def find_snaps_walk_manually():
paths = []
stack = [base_path]
while stack:
current = stack.pop()
for child in os.scandir(current):
if child.is_dir():
stack.append(child)
elif child.name == file_to_find:
paths.append(pathlib.Path(child.path))
return paths
Я также попробовал обернуть ящики Rust walkdir
и jwalk
PyO3 — они были не намного быстрее, чем find_snaps_walk_manually
(около 13,14/13,34 RPS).
да, кстати, scandir был поздней реализацией, которая использовала интерактивные API ОС, а не просто быстро получала все результаты из os.walk. Я думаю, что в pathlib Path.iterdir
это использует. Он должен нормально работать с многопоточностью (даже с многопоточностью GILocked Python).
glob._Globber
(что лежит в основе Path.rglob()
) также использует scandir
. Но учитывая, что даже реализации на основе Rust примерно такие же быстрые, я думаю, что мы почти достигли предела того, что базовые синхронные системные вызовы могут вытолкнуть в пользовательское пространство. Следующим шагом будет что-то вроде io_uring getdents...
Существуют ли оптимизации или альтернативные библиотеки/методы, которые могут помочь повысить производительность?
Да, обновление до более новой версии Python должно обеспечить существенное улучшение производительности. Python 3.13 (в настоящее время находится в бета-версии) включает наибольшее ускорение.
(Я уже пытался дать этот ответ, но несколько модераторов SO бросились мне в глотку. Если они сделают это снова, я впредь не буду предоставлять никакой поддержки pathlib для SO.)
Хороший комментарий! Я попробовал свои примеры на 3.13b0, и rglob
работает значительно быстрее, чем в 3.12, но find_snaps_walk_manually
по-прежнему с небольшим преимуществом (13,13 ips против 12,19 ips).
Честно говоря, я бы, наверное, посмотрел на обертку чего-то вроде crates.io/crates/jwalk с PyO3, если вам действительно нужна вся возможная скорость.