有没有更快的方法在.NET中递归扫描目录?

时间:2009-04-07 04:33:23

标签: c# .net filesystems

我正在.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手动编码,但是如果有更快的内置方式,我想避免这种情况。

9 个答案:

答案 0 :(得分:38)

这种需要稍微调整的实现速度要快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;
    }

答案 1 :(得分:7)

.NET文件枚举方法的历史很长。问题是没有一种枚举大型目录结构的即时方法。即使是这里接受的答案也存在GC分配的问题。

我能做的最好的事情已经包含在我的库中,并在FileFile中作为sourceCSharpTest.Net.IO namespace)类公开。此类可以枚举文件和文件夹,而无需不必要的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秒。

您的里程可能会因驱动器速度而异,但这是我在托管代码中枚举文件时发现的最快的方法。 event参数是FindFile.FileFoundEventArgs类型的变异类,因此请确保不要对其进行引用,因为它的值会因为每个引发的事件而发生变化。

您可能还会注意到,DateTime的公开仅以UTC格式显示。原因是转换到当地时间是半昂贵的。您可以考虑使用UTC时间来提高性能,而不是将这些转换为本地时间。

答案 2 :(得分:5)

根据您尝试削减函数的时间,可能值得您直接调用Win32 API函数,因为现有的API会执行大量额外处理以检查您可能不是感兴趣的。

如果您还没有这样做,并且假设您不打算为Mono项目做出贡献,我强烈建议您下载Reflector并查看Microsoft如何实现API调用您是目前使用。这将让您了解需要打电话的内容以及可以省去的内容。

例如,您可以选择创建一个yield目录名而不是返回列表的函数的迭代器,这样您最终不会迭代同一个名称列表中的两个或三个通过所有不同级别的代码。

答案 3 :(得分:2)

  

它相当浅,371 dirs   每个目录中平均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();
        }
    }

我在许多不同的目录上运行了上面的代码。在我的机器上,由于运行时或文件系统的缓存,第二次扫描目录树的调用通常更快。请注意,这个系统并不是特别的,只是一个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类。

答案 4 :(得分:2)

我刚碰过这个。很好地实现了原生版本。

此版本虽然仍比使用FindFirstFindNext的版本慢,但比原始.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;
    }

它应该生成与您的本机版本相同的输出。我的测试表明,此版本的使用时间约为使用FindFirstFindNext的版本的1.7倍。在没有附带调试器的情况下运行的发布模式下获得的计时。

奇怪的是,将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秒内运行。我想如果我经常运行这些扫描,我会改变主意。

答案 5 :(得分:1)

我会使用或基于这个多线程库:http://www.codeproject.com/KB/files/FileFind.aspx

答案 6 :(得分:0)

试试这个(即首先进行初始化,然后重用你的列表和你的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; 
}

答案 7 :(得分:0)

最近我有同样的问题,我认为将所有文件夹和文件输出到文本文件中,然后使用streamreader读取文本文件,执行您想要使用多线程处理的内容也很好。

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

[更新] 嗨,白鲸,你是对的。 由于回读输出文本文件的开销,我的方法较慢。 实际上我花了一些时间来测试最佳答案和cmd.exe与200万个文件。

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

最佳答案方法(53023)比cmd.exe(64907)更快,更不用说如何改进阅读输出文本文件。虽然我最初的观点是提供一个不太糟糕的答案,但仍然感到抱歉,哈哈。

答案 8 :(得分:0)

我最近(2020年)发现此帖子是因为需要对慢速连接中的文件和目录进行计数,这是我能想到的最快的实现。 .NET枚举方法(GetFiles(),GetDirectories())执行了很多幕后工作,相比之下它们大大降低了速度。

该解决方案利用Win32 API和.NET的Parallel.ForEach()来利用线程池来最大化性能。

P /调用:

/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilew
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr FindFirstFile(
    string lpFileName,
    ref WIN32_FIND_DATA lpFindFileData
    );

/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findnextfilew
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FindNextFile(
    IntPtr hFindFile,
    ref WIN32_FIND_DATA lpFindFileData
    );

/// <summary>
/// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool FindClose(
    IntPtr hFindFile
    );

方法:

public static Tuple<long, long> CountFilesDirectories(
    string path,
    CancellationToken token
    )
{
    if (String.IsNullOrWhiteSpace(path))
        throw new ArgumentNullException("path", "The provided path is NULL or empty.");

    // If the provided path doesn't end in a backslash, append one.
    if (path.Last() != '\\')
        path += '\\';

    IntPtr hFile = IntPtr.Zero;
    Win32.Kernel32.WIN32_FIND_DATA fd = new Win32.Kernel32.WIN32_FIND_DATA();

    long files = 0;
    long dirs = 0;

    try
    {
        hFile = Win32.Kernel32.FindFirstFile(
            path + "*", // Discover all files/folders by ending a directory with "*", e.g. "X:\*".
            ref fd
            );

        // If we encounter an error, or there are no files/directories, we return no entries.
        if (hFile.ToInt64() == -1)
            return Tuple.Create<long, long>(0, 0);

        //
        // Find (and count) each file/directory, then iterate through each directory in parallel to maximize performance.
        //

        List<string> directories = new List<string>();

        do
        {
            // If a directory (and not a Reparse Point), and the name is not "." or ".." which exist as concepts in the file system,
            // count the directory and add it to a list so we can iterate over it in parallel later on to maximize performance.
            if ((fd.dwFileAttributes & FileAttributes.Directory) != 0 &&
                (fd.dwFileAttributes & FileAttributes.ReparsePoint) == 0 &&
                fd.cFileName != "." && fd.cFileName != "..")
            {
                directories.Add(System.IO.Path.Combine(path, fd.cFileName));
                dirs++;
            }
            // Otherwise, if this is a file ("archive"), increment the file count.
            else if ((fd.dwFileAttributes & FileAttributes.Archive) != 0)
            {
                files++;
            }
        }
        while (Win32.Kernel32.FindNextFile(hFile, ref fd));

        // Iterate over each discovered directory in parallel to maximize file/directory counting performance,
        // calling itself recursively to traverse each directory completely.
        Parallel.ForEach(
            directories,
            new ParallelOptions()
            {
                CancellationToken = token
            },
            directory =>
            {
                var count = CountFilesDirectories(
                    directory,
                    token
                    );

                lock (directories)
                {
                    files += count.Item1;
                    dirs += count.Item2;
                }
            });
    }
    catch (Exception)
    {
        // Handle as desired.
    }
    finally
    {
        if (hFile.ToInt64() != 0)
            Win32.Kernel32.FindClose(hFile);
    }

    return Tuple.Create<long, long>(files, dirs);
}

在我的本地系统上,GetFiles()/ GetDirectories()的性能可以接近此水平,但是在速度较慢的连接(VPN等)上,我发现它的速度要快得多,访问时间从45分钟缩短到90秒约40k文件的远程目录,约40 GB。

这也可以很容易地修改为包括其他数据,例如所有已计数文件的总文件大小,或者从最远的分支开始快速递归遍历并删除空目录。

相关问题