更快的目录枚举器
描述如何为目录中所有文件的属性创建一个明显更快的枚举器。
引言
.NET Framework 的 Directory
类包含用于查询目录中文件列表的方法。对于每个文件,您还可以查询文件的属性,例如大小、创建日期等。但是,当查询远程 PC 上的文件时,这可能效率很低;需要进行可能代价高昂的网络往返才能检索每个文件的属性。本文描述了一种更有效的实现,其速度大约快 3 倍。
背景
假设您正在编写一个需要找到目录中最近修改的文件的应用程序。为了实现这一点,您可能有一个类似于以下的函数
DateTime GetLastFileModifiedSlow(string dir)
{
DateTime retval = DateTime.MinValue;
string [] files = Directory.GetFiles(dir);
for (int i=0; i<files.Length; i++)
{
DateTime lastWriteTime = File.GetLastWriteTime(files[i]);
if (lastWriteTime > retval)
{
retval = lastWriteTime;
}
}
return retval;
}
该函数当然可以工作,但它有一些非常差的性能特征
GetFiles
必须分配一个可能非常大的数组。GetFiles
必须等待整个目录的条目返回后才能返回。- 对于每个文件,都会向文件系统发送一个可能代价高昂的查询。没有尝试执行任何类型的批量查询。
您可能认为转换为 DirectoryInfo.GetFileSystemInfos
将改善第 3 项
DateTime GetLastFileModifiedSlow2(string dir)
{
DateTime retval = DateTime.MinValue;
DirectoryInfo dirInfo = new DirectoryInfo(dir);
FileInfo[] files = dirInfo.GetFiles();
for (int i=0; i<files.Length; i++)
{
if (files[i].LastWriteTime > retval)
{
retval = lastWriteTime;
}
}
return retval;
}
但这并没有改变任何事情:GetFiles()
返回的对象没有用任何数据初始化,并且在第一次访问任何属性时都会查询文件系统。
使其更快
附加的测试应用程序在 FastDirectoryEnumerator.cs 中包含了 FastDirectoryEnumerator
类。使用 GetFiles
方法,我们可以编写与第一个慢方法等效的方法。
DateTime GetLastFileModifiedFast(string dir)
{
DateTime retval = DateTime.MinValue;
FileData [] files = FastDirectoryEnumerator.GetFiles(dir);
for (int i=0; i<files.Length; i++)
{
if (files[i].LastWriteTime > retval)
{
retval = lastWriteTime;
}
}
return retval;
}
FileData
对象提供了 FileInfo
类提供的文件的所有标准属性。
使其更快
使用 EnumerateFiles
方法的其中一个重载来枚举目录中的所有文件。枚举返回一个 FileData
对象。
以下是使用 FastDirectoryEnumerator
的相同方法的示例
DateTime GetLastFileModifiedFast(string dir)
{
DateTime retval = DateTime.MinValue;
foreach (FileData f in FastDirectoryEnumerator.EnumerateFiles(dir))
{
if (f.LastWriteTime > retval)
{
retval = f.LastWriteTime;
}
}
return retval;
}
性能
测试应用程序允许您在目录中创建大量文件,然后测试使用所有三种方法进行枚举所需的时间。我使用了一个包含 3000 个文件的目录,并对每个测试运行了三次以给出每个测试的最佳答案。
使用本地硬盘驱动器上的路径会导致以下时间
Directory.GetFiles
方法:~225 毫秒DirectoryInfo.GetFiles
方法:~230 毫秒FastDirectoryEnumerator.GetFiles
方法:~33 毫秒FastDirectoryEnumerator.EnumerateFiles
方法:~27 毫秒
这大约是速度最快和最慢方法之间性能的 8.5 倍提升。当文件位于 UNC 路径上时,性能提升更加明显。对于此测试,我使用了与之前的测试相同的目录。唯一的区别是,我通过 UNC 共享名称而不是本地路径引用了该目录。在测试时,我连接到我的家庭无线网络。
Directory.GetFiles
方法:~43,860 毫秒DirectoryInfo.GetFiles
方法:~44,000 毫秒FastDirectoryEnumerator.GetFiles
方法:~55 毫秒FastDirectoryEnumerator.EnumerateFiles
方法:~53 毫秒
这大约是性能的 830 倍提升,超过了两个数量级!而且,随着包含文件的 PC 的延迟增加,差距只会越来越大。
为什么更快?
如上所述,Directory.GetFiles
和 DirectoryInfo.GetFiles
存在许多缺点。最重要的是它们会丢弃信息,并且不能有效地允许您同时检索有关多个文件的信息。
在内部,Directory.GetFiles
被实现为 Win32 FindFirstFile/FindNextFile
函数的包装器。这些函数都返回有关每个已枚举文件的信息,而 GetFiles()
方法在返回文件名时会丢弃这些信息。它们还使用单个网络消息检索有关多个文件的信息。
FastDirectoryEnumerator
保留了此信息,并在 FileData
类中返回它。这大大减少了完成相同任务所需的网络往返次数。
历史
- 2009 年 8 月 13 日:初始版本。
- 2009 年 8 月 14 日:添加了安全检查、参数检查和
GetFiles
方法。 - 2009 年 8 月 24 日:修复了使用
GetFiles
的 AllDirectories 搜索。删除了有关 .NET 4.0 包含类似内容的说明。 - 2009 年 9 月 8 日:修复了当过滤器不是 * 或 *.* 时 AllDirectories 搜索的问题。