Есть ли более быстрый способ сканирования через каталог рекурсивно в .NET?

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

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

   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, но хотелось бы избежать этого, если есть встроенный способ, который выполняется быстрее.

Ответ 1

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

    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);
        }
    }

interop defs:

    [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;
    }

Ответ 2

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

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

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

Ответ 3

Существует долгая история медленного процесса перечисления файлов .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, поэтому не забудьте ссылаться на него, поскольку значения будут меняться для каждого события поднят.

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

Ответ 4

Его довольно мелкие 371 дир в среднем по 10 файлов в каждом каталоге. некоторые каналы содержат другие вспомогательные устройства

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

    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();
        }
    }

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

// 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 используется для синхронизации.

Ответ 5

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

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

Ответ 7

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

  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; 
}

Ответ 8

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

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), не говоря уже о том, как улучшить чтение текстового файла вывода. Хотя мой первоначальный момент состоит в том, чтобы дать не слишком плохой ответ, все еще жаль, ха.