在.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类用于计时。
我只是碰到了这个。 本地版本的好实施。
这个版本虽然比使用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; }
它应该产生与你的本地版本相同的输出。 我的testing表明,这个版本大约是使用FindFirst
和FindNext
的版本的1.7倍。 在发布模式下运行的时间不需要附加debugging器就可以运行。
奇怪的是,将GetFileSystemInfos
更改为EnumerateFileSystemInfos
GetFileSystemInfos
testing中的运行时间增加大约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)更快,更不用说如何提高阅读输出文本文件。 虽然我原来的意思是提供一个不错的答案,还是觉得对不起,哈哈。