Есть ли более быстрый способ рекурсивного сканирования каталога в .NET? - PullRequest
27 голосов
/ 07 апреля 2009

Я пишу сканер каталогов в .NET.

Для каждого файла / каталога мне нужна следующая информация.

   class Info {
        public bool IsDirectory;
        public string Path;
        public DateTime ModifiedDate;
        public DateTime CreatedDate;
    }

У меня есть эта функция:

      static List<Info> RecursiveMovieFolderScan(string path){

        var info = new List<Info>();
        var dirInfo = new DirectoryInfo(path);
        foreach (var dir in dirInfo.GetDirectories()) {
            info.Add(new Info() {
                IsDirectory = true,
                CreatedDate = dir.CreationTimeUtc,
                ModifiedDate = dir.LastWriteTimeUtc,
                Path = dir.FullName
            });

            info.AddRange(RecursiveMovieFolderScan(dir.FullName));
        }

        foreach (var file in dirInfo.GetFiles()) {
            info.Add(new Info()
            {
                IsDirectory = false,
                CreatedDate = file.CreationTimeUtc,
                ModifiedDate = file.LastWriteTimeUtc,
                Path = file.FullName
            });
        }

        return info; 
    }

Оказывается, эта реализация довольно медленная. Есть ли способ ускорить это? Я думаю о ручном кодировании этого с FindFirstFileW, но хотел бы избежать этого, если есть встроенный способ, который быстрее.

Ответы [ 8 ]

39 голосов
/ 07 апреля 2009

Эта реализация, которая требует небольшой настройки, в 5-10 раз быстрее.

    static List<Info> RecursiveScan2(string directory) {
        IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
        WIN32_FIND_DATAW findData;
        IntPtr findHandle = INVALID_HANDLE_VALUE;

        var info = new List<Info>();
        try {
            findHandle = FindFirstFileW(directory + @"\*", out findData);
            if (findHandle != INVALID_HANDLE_VALUE) {

                do {
                    if (findData.cFileName == "." || findData.cFileName == "..") continue;

                    string fullpath = directory + (directory.EndsWith("\\") ? "" : "\\") + findData.cFileName;

                    bool isDir = false;

                    if ((findData.dwFileAttributes & FileAttributes.Directory) != 0) {
                        isDir = true;
                        info.AddRange(RecursiveScan2(fullpath));
                    }

                    info.Add(new Info()
                    {
                        CreatedDate = findData.ftCreationTime.ToDateTime(),
                        ModifiedDate = findData.ftLastWriteTime.ToDateTime(),
                        IsDirectory = isDir,
                        Path = fullpath
                    });
                }
                while (FindNextFile(findHandle, out findData));

            }
        } finally {
            if (findHandle != INVALID_HANDLE_VALUE) FindClose(findHandle);
        }
        return info;
    }

метод расширения:

 public static class FILETIMEExtensions {
        public static DateTime ToDateTime(this System.Runtime.InteropServices.ComTypes.FILETIME filetime ) {
            long highBits = filetime.dwHighDateTime;
            highBits = highBits << 32;
            return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime);
        }
    }

значения взаимодействия:

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern IntPtr FindFirstFileW(string lpFileName, out WIN32_FIND_DATAW lpFindFileData);

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATAW lpFindFileData);

    [DllImport("kernel32.dll")]
    public static extern bool FindClose(IntPtr hFindFile);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct WIN32_FIND_DATAW {
        public FileAttributes dwFileAttributes;
        internal System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
        internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
        internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
        public int nFileSizeHigh;
        public int nFileSizeLow;
        public int dwReserved0;
        public int dwReserved1;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string cFileName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
        public string cAlternateFileName;
    }
7 голосов
/ 17 сентября 2012

Существует долгая история медленных методов перечисления файлов .NET. Проблема в том, что нет мгновенного способа перечисления больших структур каталогов. Даже в принятом ответе есть проблемы с распределением GC.

Лучшее, что я смог сделать, - это обернуть его в моей библиотеке и представить как класс FileFile ( source ) в пространстве имен CSharpTest.Net.IO . Этот класс может перечислять файлы и папки без ненужных выделений GC и маршалинга строк.

Использование достаточно простое, а свойство RaiseOnAccessDenied будет пропускать каталоги и файлы, к которым у пользователя нет доступа:

    private static long SizeOf(string directory)
    {
        var fcounter = new CSharpTest.Net.IO.FindFile(directory, "*", true, true, true);
        fcounter.RaiseOnAccessDenied = false;

        long size = 0, total = 0;
        fcounter.FileFound +=
            (o, e) =>
            {
                if (!e.IsDirectory)
                {
                    Interlocked.Increment(ref total);
                    size += e.Length;
                }
            };

        Stopwatch sw = Stopwatch.StartNew();
        fcounter.Find();
        Console.WriteLine("Enumerated {0:n0} files totaling {1:n0} bytes in {2:n3} seconds.",
                          total, size, sw.Elapsed.TotalSeconds);
        return size;
    }

Для моего локального диска C: \ это выводит следующее:

Перечислено 810 046 файлов общим объемом 307 707 792 662 байта за 232 876 секунд.

Ваш пробег может варьироваться в зависимости от скорости диска, но это самый быстрый метод перечисления файлов в управляемом коде. Параметр события является мутирующим классом типа FindFile.FileFoundEventArgs , поэтому убедитесь, что вы не сохраняете ссылку на него, так как его значения будут изменяться для каждого вызванного события.

Вы также можете заметить, что дата выставлена ​​только в UTC. Причина в том, что перевод на местное время стоит недорого. Возможно, вы захотите использовать время UTC для повышения производительности, а не преобразовывать их в местное время.

5 голосов
/ 07 апреля 2009

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

Если вы еще этого не сделали и, если вы не собираетесь вносить свой вклад в проект Mono, я настоятельно рекомендую загрузить Reflector и узнать, как Microsoft реализовала API-интерфейс, призывает вас в настоящее время используете. Это даст вам представление о том, что вам нужно назвать и что вы можете пропустить.

Вы можете, например, выбрать создание итератора, который yield имен каталогов вместо функции, возвращающей список, таким образом, вы не закончите итерацию по одному и тому же списку имен два или три раза все различные уровни кода.

2 голосов
/ 20 апреля 2012

Я только что столкнулся с этим. Хорошая реализация родной версии.

Эта версия, хотя и медленнее, чем версия, использующая FindFirst и FindNext, немного быстрее, чем ваша оригинальная версия .NET.

    static List<Info> RecursiveMovieFolderScan(string path)
    {
        var info = new List<Info>();
        var dirInfo = new DirectoryInfo(path);
        foreach (var entry in dirInfo.GetFileSystemInfos())
        {
            bool isDir = (entry.Attributes & FileAttributes.Directory) != 0;
            if (isDir)
            {
                info.AddRange(RecursiveMovieFolderScan(entry.FullName));
            }
            info.Add(new Info()
            {
                IsDirectory = isDir,
                CreatedDate = entry.CreationTimeUtc,
                ModifiedDate = entry.LastWriteTimeUtc,
                Path = entry.FullName
            });
        }
        return info;
    }

Он должен выдавать тот же вывод, что и ваша нативная версия. Мои тесты показывают, что эта версия занимает примерно в 1,7 раза больше, чем версия, которая использует FindFirst и FindNext. Время получено в режиме релиза без отладчика.

Любопытно, что изменение GetFileSystemInfos на EnumerateFileSystemInfos добавляет около 5% времени выполнения в моих тестах. Я скорее ожидал, что он будет работать с той же скоростью или, возможно, быстрее, потому что ему не нужно было создавать массив объектов FileSystemInfo.

Следующий код еще короче, потому что он позволяет Framework заботиться о рекурсии. Но это на 15-20% медленнее, чем версия выше.

    static List<Info> RecursiveScan3(string path)
    {
        var info = new List<Info>();

        var dirInfo = new DirectoryInfo(path);
        foreach (var entry in dirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
        {
            info.Add(new Info()
            {
                IsDirectory = (entry.Attributes & FileAttributes.Directory) != 0,
                CreatedDate = entry.CreationTimeUtc,
                ModifiedDate = entry.LastWriteTimeUtc,
                Path = entry.FullName
            });
        }
        return info;
    }

Опять же, если вы измените это значение на GetFileSystemInfos, оно будет немного (но только немного) быстрее.

Для моих целей первое решение, приведенное выше, достаточно быстрое. Нативная версия запускается примерно за 1,6 секунды. Версия, которая использует DirectoryInfo, запускается примерно за 2,9 секунды. Полагаю, если бы я часто проводил эти сканы, я бы передумал.

2 голосов
/ 08 апреля 2009

Это довольно неглубоко, 371 dirs с в среднем 10 файлов в каждом каталоге. некоторые dirs содержат другие sub dirs

Это всего лишь комментарий, но ваши цифры кажутся довольно высокими. Я запустил ниже, используя, по сути, тот же рекурсивный метод, который вы используете, и мое время намного меньше, несмотря на создание вывода строки.

    public void RecurseTest(DirectoryInfo dirInfo, 
                            StringBuilder sb, 
                            int depth)
    {
        _dirCounter++;
        if (depth > _maxDepth)
            _maxDepth = depth;

        var array = dirInfo.GetFileSystemInfos();
        foreach (var item in array)
        {
            sb.Append(item.FullName);
            if (item is DirectoryInfo)
            {
                sb.Append(" (D)");
                sb.AppendLine();

                RecurseTest(item as DirectoryInfo, sb, depth+1);
            }
            else
            { _fileCounter++; }

            sb.AppendLine();
        }
    }

Я запускал приведенный выше код для ряда разных каталогов. На моей машине второй вызов для сканирования дерева каталогов обычно выполнялся быстрее из-за кэширования в среде выполнения или в файловой системе. Обратите внимание, что эта система не является чем-то особенным, это всего лишь 1-летняя рабочая станция для разработки.

// cached call
Dirs = 150, files = 420, max depth = 5
Time taken = 53 milliseconds

// cached call
Dirs = 1117, files = 9076, max depth = 11
Time taken = 433 milliseconds

// first call
Dirs = 1052, files = 5903, max depth = 12
Time taken = 11921 milliseconds

// first call
Dirs = 793, files = 10748, max depth = 10
Time taken = 5433 milliseconds (2nd run 363 milliseconds)

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

// now grabbing last update and creation time.
Dirs = 150, files = 420, max depth = 5
Time taken = 103 milliseconds (2nd run 93 milliseconds)

Dirs = 1117, files = 9076, max depth = 11
Time taken = 992 milliseconds (2nd run 984 milliseconds)

Dirs = 793, files = 10748, max depth = 10
Time taken = 1382 milliseconds (2nd run 735 milliseconds)

Dirs = 1052, files = 5903, max depth = 12
Time taken = 936 milliseconds (2nd run 595 milliseconds)

Примечание: класс System.Diagnostics.StopWatch, используемый для синхронизации.

1 голос
/ 07 апреля 2009

Я бы использовал или основывал себя на этой многопоточной библиотеке: http://www.codeproject.com/KB/files/FileFind.aspx

0 голосов
/ 07 августа 2014

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

cmd.exe /u /c dir "M:\" /s /b >"c:\flist1.txt"

[обновление] Привет, Моби, ты прав. Мой подход медленнее из-за накладных расходов на чтение выходного текстового файла. На самом деле я потратил некоторое время, чтобы проверить топ-ответ и cmd.exe с 2 миллионами файлов.

The top answer: 2010100 files, time: 53023
cmd.exe method: 2010100 files, cmd time: 64907, scan output file time: 19832.

Метод верхнего ответа (53023) быстрее, чем cmd.exe (64907), не говоря уже о том, как улучшить чтение выходного текстового файла. Хотя моя первоначальная цель заключается в том, чтобы дать неплохой ответ, все же пожалею, ха.

0 голосов
/ 07 апреля 2009

попробуйте это (т.е. сначала выполните инициализацию, а затем повторно используйте ваш список и ваши объекты directoryInfo):

  static List<Info> RecursiveMovieFolderScan1() {
      var info = new List<Info>();
      var dirInfo = new DirectoryInfo(path);
      RecursiveMovieFolderScan(dirInfo, info);
      return info;
  } 

  static List<Info> RecursiveMovieFolderScan(DirectoryInfo dirInfo, List<Info> info){

    foreach (var dir in dirInfo.GetDirectories()) {

        info.Add(new Info() {
            IsDirectory = true,
            CreatedDate = dir.CreationTimeUtc,
            ModifiedDate = dir.LastWriteTimeUtc,
            Path = dir.FullName
        });

        RecursiveMovieFolderScan(dir, info);
    }

    foreach (var file in dirInfo.GetFiles()) {
        info.Add(new Info()
        {
            IsDirectory = false,
            CreatedDate = file.CreationTimeUtc,
            ModifiedDate = file.LastWriteTimeUtc,
            Path = file.FullName
        });
    }

    return info; 
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...