在.NET中recursion地扫描目录有更快的方法吗?

我在.NET中编写一个目录扫描器。

对于每个文件/目录我需要以下信息。

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

我有这个function:

  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来编写这个代码,但是如果有一个更快的内置方法,我想避免这种情况。

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

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

.NET文件枚举方法的历史悠久。 问题是没有一个枚举大型目录结构的即时方法。 即使是接受的答案,这也是GC分配的问题。

我已经能够做的最好的事情是封装在我的库中,并作为CSharpTest.Net.IO命名空间中的FileFile ( source )类公开 。 这个类可以枚举文件和文件夹,而不需要GC分配和string编组。

用法很简单,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:\驱动器,输出如下内容:

在232.876秒内枚举了总共307,707,792,662个字节的810,046个文件。

您的里程可能因驱动器速度而异,但这是我用托pipe代码枚举文件的最快方法。 事件参数是types为FindFile.FileFoundEventArgs的变异类,所以请确保您不要保留对它的引用,因为它的值会随着引发的每个事件而改变。

你也可能注意到DateTime的暴露只在UTC。 原因是转换到当地时间是半价的。 您可能会考虑使用UTC时间来提高性能,而不是将其转换为当地时间。

根据您试图削减函数的时间,可能需要一段时间直接调用Win32 API函数,因为现有的API会执行大量额外的处理来检查您可能不感兴趣的事情。

如果您还没有这样做,并且假设您不打算贡献Mono项目,我强烈build议下载Reflector并查看Microsoft如何实施您当前正在使用的API调用。 这会让你知道你需要打电话什么,你可以省略。

例如,你可能会select创build一个yield目录名称的迭代器,而不是返回一个列表的函数,这样你不会最终迭代遍历所有不同级别的码。

它很浅,371个目录,平均每个目录有10个文件。 一些dirs包含其他子dirs

这只是一个评论,但你的数字似乎相当高。 我运行下面使用基本上相同的recursion方法,你正在使用和我的时间是远远低于尽pipe创buildstring输出。

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

我在许多不同的目录上运行上面的代码。 在我的机器上,第二次扫描目录树的调用通常由于运行时或文件系统的caching而更快。 请注意,这个系统没有什么特别之处,只是一个1年前的开发工作站。

 //caching的调用
 Dirs = 150,文件= 420,最大深度= 5
所用时间= 53毫秒

 //caching的调用
 Dirs = 1117,files = 9076,最大深度= 11
所用时间= 433毫秒

 //第一个电话
 Dirs = 1052,files = 5903,最大深度= 12
所用时间= 11921毫秒

 //第一个电话
 Dirs = 793,files = 10748,最大深度= 10
所用时间= 5433毫秒(第二次运行363毫秒)

担心我没有得到创build和修改date,代码被修改以输出以及以下时间。

 //现在抓取上次更新和创build时间
 Dirs = 150,文件= 420,最大深度= 5
所用时间= 103毫秒(第二次运行93毫秒)

 Dirs = 1117,files = 9076,最大深度= 11
所用时间= 992毫秒(第二次运行984毫秒)

 Dirs = 793,files = 10748,最大深度= 10
所用时间= 1382毫秒(第二次运行735毫秒)

 Dirs = 1052,files = 5903,最大深度= 12
所用时间= 936毫秒(第二次运行595毫秒)

注意:System.Diagnostics.StopWatch类用于计时。

我只是碰到了这个。 本地版本的好实施。

这个版本虽然比使用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; } 

它应该产生与你的本地版本相同的输出。 我的testing表明,这个版本大约是使用FindFirstFindNext的版本的1.7倍。 在发布模式下运行的时间不需要附加debugging器就可以运行。

奇怪的是,将GetFileSystemInfos更改为EnumerateFileSystemInfos GetFileSystemInfostesting中的运行时间增加大约5%。 我宁可期望它以相同的速度运行或可能更快,因为它不必创buildFileSystemInfo对象的数组。

下面的代码仍然更短,因为它让框架照顾recursion。 但是比上面的版本慢了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 ,则会略微(但稍微)更快。

就我而言,上面的第一个解决scheme是相当快的。 本地版本运行约1.6秒。 使用DirectoryInfo的版本在大约2.9秒内运行。 我想如果我经常运行这些扫描,我会改变主意。

我会使用或基于自己的multithreading库: http : //www.codeproject.com/KB/files/FileFind.aspx

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

最近我有同样的问题,我认为把所有的文件夹和文件输出到一个文本文件也是好的,然后使用streamreader读取文本文件,做你想用multithreading处理。

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

[更新]嗨莫比,你是对的。 由于回读输出文本文件的开销,我的方法较慢。 其实我花了一些时间来testing最高的答案和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)更快,更不用说如何提高阅读输出文本文件。 虽然我原来的意思是提供一个不错的答案,还是觉得对不起,哈哈。