获取目录大小的更高效方法

9

我已经编写了一个递归函数来计算文件夹路径的目录大小。它可以工作,但是随着我需要搜索的目录数量和每个相应文件夹中文件的数量增长,这是一种非常缓慢和低效的方法。

static string GetDirectorySize(string parentDir)
{
    long totalFileSize = 0;

    string[] dirFiles = Directory.GetFiles(parentDir, "*.*", 
                            System.IO.SearchOption.AllDirectories);

    foreach (string fileName in dirFiles)
    {
        // Use FileInfo to get length of each file.
        FileInfo info = new FileInfo(fileName);
        totalFileSize = totalFileSize + info.Length;
    }
    return String.Format(new FileSizeFormatProvider(), "{0:fs}", totalFileSize);
}

这将在所有子目录中搜索参数路径,因此dirFiles数组会变得非常大。有更好的方法来实现这个吗?我已经搜索了一下,但还没有找到合适的方法。

我想到的另一个方法是将结果放入缓存中,当再次调用函数时,尝试查找差异,并仅重新搜索已更改的文件夹。不确定这是否可行...


1
这是一个比你想象中更复杂的问题。我建议调用Win32 API方法来处理这样的事情。 - asawyer
https://dev59.com/gnVC5IYBdhLWcg3w-mVO - Tim Schmelter
请查看这个并行解决方案:https://dev59.com/znA85IYBdhLWcg3wBOzh - Sergey Berezovskiy
2
数组大小并不重要,99.9% 的成本在于磁盘访问。你至少需要支付一次,之后可以通过 FileSystemWatcher 获取增量更新。 - Hans Passant
5个回答

26

首先,您需要扫描树以获取所有文件的列表。然后,要获取每个文件的大小,您需要重新打开它们。这相当于进行了两次扫描。

我建议您使用 DirectoryInfo.GetFiles,可以直接获取 FileInfo 对象。这些对象已经预先填充了其长度信息。

.NET 4 中,您还可以使用 EnumerateFiles 方法,该方法会返回一个惰性的 IEnumable


它们不是预填充的,仍然需要往返磁盘。这是必要的,因为您不希望使用过时的数据。而 EnumerateFiles 被添加到 .NET 4 的原因就在于此。 - Hans Passant
至少在.NET 4中它们是预填充的。这发生在FileInfoResultHandler.CreateObject调用FileInfo.InitializeFrom调用PopulateFrom(WIN32_FIND_DATA)时。请撤销您的反对票,此答案是正确的。 - usr
1
这不是我的投票。留下评论并且给出负面评价并不是一个健康的策略 :) - Hans Passant
@HansPassant:“如果FileInfo对象的当前实例是从以下任何DirectoryInfo方法之一返回的:... EnumerateFiles”,则Length属性的值将被预缓存。http://msdn.microsoft.com/en-us/library/system.io.fileinfo.length.aspx +1 usr - paparazzo

13

这段代码更加晦涩难懂,但对于10,000次执行只需要约2秒钟。

    public static long GetDirectorySize(string parentDirectory)
    {
        return new DirectoryInfo(parentDirectory).GetFiles("*.*", SearchOption.AllDirectories).Sum(file => file.Length);
    }

9
"*.*会漏掉一些文件。" - Joshua Evensen
2
为了清晰起见,.Sum() 需要 System.Linq。 - MonoThreaded

12
尝试一下。
        DirectoryInfo DirInfo = new DirectoryInfo(@"C:\DataLoad\");
        Stopwatch sw = new Stopwatch();
        try
        {
            sw.Start();
            Int64 ttl = 0;
            Int32 fileCount = 0;
            foreach (FileInfo fi in DirInfo.EnumerateFiles("*", SearchOption.AllDirectories))
            {
                ttl += fi.Length;
                fileCount++;
            }
            sw.Stop();
            Debug.WriteLine(sw.ElapsedMilliseconds.ToString() + " " + fileCount.ToString());
        }
        catch (Exception Ex)
        {
            Debug.WriteLine(Ex.ToString());
        }

这在桌面非RAID P4上只用了70秒就完成了70万次操作。大约每秒钟可以处理1万个操作。在服务器级别的机器上,应该很容易达到每秒处理10万个以上的速度。

正如usr(+1)所说,EnumerateFile已经预先填好了长度。


4
你可以使用EnumerateFiles()代替GetFiles()来加速函数,这样至少不会在内存中加载完整的列表。
如果还不够快,你可以使用线程使函数更复杂(每个目录一个线程可能过多,但没有通用规则)。
您可以使用固定数量的线程从队列中获取目录,每个线程计算目录的大小并添加到总数。类似以下步骤:
- 获取所有目录(而不是文件)的列表。 - 创建N个线程(例如每个核心一个线程)。 - 每个线程获取一个目录并计算其大小。 - 如果队列中没有其他目录,则线程结束。 - 如果队列中有目录,则计算其大小等等。 - 当所有线程终止时,函数结束。
你可以大大改进跨越所有线程搜索目录的算法(例如,当一个线程解析一个目录时,它会将文件夹添加到队列中)。如果你发现速度太慢,可以让它变得更加复杂(这项任务已被微软用作Task Parallel Library的示例)。

+1. 注意,线程和IO绑定任务会产生奇怪的性能结果 - 您必须进行原型设计和测量。 - Alexei Levenkov
当然可以!我认为选择正确的线程数量比编写代码更棘手。我猜这很大程度上取决于磁盘随机访问的性能。无论我如何计算,我都无法像Windows那样快,我想可能有一些诀窍...在某个地方... - Adriano Repetti
由于此处受到I/O限制,我不太确定添加额外线程会带来多少收益(如果有的话)。 - paparazzo
使用4个线程的池与不使用线程的相同解决方案(EnumerateFiles)相比,可以将速度至少提高两倍。由于硬件的差异,这可能会有很大的变化。当Windows读取数据块(目录)时,它不仅会读取少量的字节,而是读取整个块并将其保存在缓存中。 - Adriano Repetti

-1
long length = Directory.GetFiles(@"MainFolderPath", "*", SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接