目录文件大小计算 - 如何加快速度?

20

使用C#,我正在查找目录的总大小。逻辑是这样的:获取文件夹内的文件。求出总大小。查找是否有子目录。然后进行递归搜索。

我尝试了另一种方法:使用FSO(obj.GetFolder(path).Size)。在这两种方法中,时间上没有太大差异。

现在的问题是,某个特定文件夹中有数万个文件,需要至少2分钟才能找到文件夹的大小。而且,如果我再次运行程序,它就会非常快(5秒)。我认为Windows正在缓存文件大小。

当我第一次运行程序时,有没有办法缩短所需的时间?


12
你的方法比“资源管理器”第一次执行这个任务慢吗? - Marc Gravell
1
我认为这很正常。你可以使用更低级别的API在文件系统级别进行递归,但我怀疑这不会显著提高速度。 - Alan
@Marc,不,它并没有显著的不同。此外,我已经尝试过WinApi,但差别不大。 - Jey Geethan
对文件系统进行碎片整理并选择分组文件夹选项将加快初始搜索速度;据我所知,没有加速方法;您可以使用固态硬盘驱动器... - bohdan_trotsenko
@MarcGravell,您能在您的机器上试一下并告诉我是否可行吗? - Jey Geethan
请查看此答案:https://dev59.com/v3RB5IYBdhLWcg3w6bYE#32364847 它的速度快了4倍。 - Ahmed Sabry
8个回答

36

如果您尝试并行化它,而且出乎意料的是,在我的机器上加速了(在四核上高达3倍),不知道在所有情况下是否有效,但可以尝试一下...

.NET4.0代码(或使用TaskParallelLibrary的3.5版本)

    private static long DirSize(string sourceDir, bool recurse)
    {
        long size = 0;
        string[] fileEntries = Directory.GetFiles(sourceDir);

        foreach (string fileName in fileEntries)
        {
            Interlocked.Add(ref size, (new FileInfo(fileName)).Length);
        }

        if (recurse)
        {
            string[] subdirEntries = Directory.GetDirectories(sourceDir);

            Parallel.For<long>(0, subdirEntries.Length, () => 0, (i, loop, subtotal) =>
            {
                if ((File.GetAttributes(subdirEntries[i]) & FileAttributes.ReparsePoint) != FileAttributes.ReparsePoint)
                {
                    subtotal += DirSize(subdirEntries[i], true);
                    return subtotal;
                }
                return 0;
            },
                (x) => Interlocked.Add(ref size, x)
            );
        }
        return size;
    }

1
至少它可能会优化用户模式操作。 - kenny
1
当我参加微软Visual Studio 2010发布会(英国Tech Days)时,用于演示新的Parallel LINQ方法的示例正是计算目录大小。如果我没记错的话,在他的四核笔记本电脑上使用PLINQ时,我们至少看到了2倍的速度提升。这个示例在这里的一个视频中,但我记不清是哪一个了:http://www.microsoft.com/uk/techdays/resources.aspx - Codesleuth
3
请问您能解释一下为什么要检查ReparsePoint吗?因为如果我注释掉那一行,速度会提高超过5倍。 - AFgone
2
@AFgone,因为我认为重新解析点不是真正的文件。MSDN:“该文件包含一个重新解析点,它是与文件或目录相关联的用户定义数据块。”但是,一如既往,这取决于您的需求和要求。 - spookycoder

10

硬盘是一个有趣的动物 - 顺序访问(例如读取一个大的连续文件)非常快,可达80兆字节/秒。然而,随机访问非常慢。这就是你遇到的问题 - 递归进入文件夹不会读取太多(在数量方面)数据,但需要许多随机读取。第二次运行看到快速表现的原因是MFT仍然在RAM中(你对缓存的想法是正确的)

我见过的最好的方法是自己扫描MFT。理念是你一次线性地读取和解析MFT,构建你需要的信息。最终结果将更接近于在一个非常满的硬盘上花费15秒。

一些好的阅读材料: NTFSInfo.exe - http://technet.microsoft.com/en-us/sysinternals/bb897424.aspx Windows Internals - http://www.amazon.com/Windows®-Internals-Including-Windows-PRO-Developer/dp/0735625301/ref=sr_1_1?ie=UTF8&s=books&qid=1277085832&sr=8-1

顺便说一句:这种方法非常复杂,因为在Windows(或我知道的任何操作系统)中没有一个很好的方法来解决这个问题 - 问题是找出哪些文件夹/文件是必需的需要磁盘上的大量头部移动。对于微软来说,要建立一个解决你描述的问题的通用解决方案是非常困难的。


7
简短的回答是不行。Windows可以通过在每次文件写入时更新目录大小和所有父目录大小来使目录大小计算更快。然而,这会使文件写入变得更慢。由于进行文件写入比读取目录大小要常见得多,因此这是一个合理的权衡。
我不确定正在解决什么确切的问题,但如果是文件系统监视,可能值得检查:http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx

2

当扫描包含数万个文件的文件夹时,使用任何方法都会导致性能下降。

  • 使用Windows API中的FindFirstFile...和FindNextFile...函数可以提供最快的访问速度。

  • 由于调用开销,即使使用Windows API函数,性能也不会提高。框架已经封装了这些API函数,因此自己做没有意义。

  • 无论使用哪种文件访问方法,如何处理结果都决定了应用程序的性能。例如,即使使用Windows API函数,更新列表框就会导致性能下降。

  • 您不能将执行速度与Windows资源管理器进行比较。根据我的实验,我认为Windows资源管理器在许多情况下直接从文件分配表中读取。

  • 我知道最快访问文件系统的方法是使用DIR命令。您不能将性能与此命令进行比较。它肯定直接从文件分配表中读取(可能使用BIOS)。

  • 是的,操作系统缓存文件访问。

建议

  • 我想知道BackupRead是否可以在您的情况下有所帮助?

  • 如果您调用DIR并捕获并解析其输出,会怎样?(您实际上并未解析,因为每个DIR行都是固定宽度,所以只需要调用子字符串。)

  • 如果您在后台线程中调用DIR /B > NULL然后运行程序会怎样?在DIR运行时,您将从缓存的文件访问中受益。


3
这是不正确的。DIR 命令并不从文件分配表中读取信息,Windows资源管理器也是如此。两者都通过Kernel32和NTDLL进行调用,并由内核模式下的文件系统驱动程序处理。我使用依赖关系检查工具(depends.exe)对cmd.exe进行了测试,发现DIR命令调用了Kernel32.dll库中的FindFirstFileW和FindNextFileW函数。因此,执行DIR命令的速度比直接调用这些函数要慢。 - Ray Burns
首先,不可能使用“depends”来确定DIR命令使用了哪些API调用。 - AMissico
1
其次,如果您使用“进程监视器”监视DIR命令,您会注意到只执行QueryDirectory操作。如果您在.NET中创建一个简单的控制台应用程序,调用GetFileSystemInfosGetDirectories,您会发现执行相同的操作更频繁,包括大量的CloseFileCreateFile操作。这些.NET方法调用API例程。因此,您可以推断DIR命令没有调用这些API函数。 - AMissico
1
第三步,请按照我的方式操作。使用C/C++创建控制台应用程序。此应用程序仅调用API例程并递归下降文件夹结构。它不输出任何内容。将其执行时间与相同的DIR命令重定向到NULL或文件进行比较。DIR命令始终更快。所有访问都必须通过文件系统驱动程序进行,但是DIR和在某些情况下Windows资源管理器直接从文件分配表中读取。请参见Chris Gray的答案。 - AMissico
最后,如果你真的想证明DIR不是直接从“fat”读取的,请使用DEBUG和debug CMD。我选择编写测试应用程序来验证我所经历的行为。我认为,DIR有一种“钩子”,允许它以“块”的形式读取文件分配表。(很可能它使用了Chris Gray答案中的技术。)没有其他解释可以解释它如此快速地从硬盘读取文件信息。 - AMissico
对于 DIR 这个想法我给出 "+1"。我的测试发现获取 .EXE 文件大小要比其他文件慢得多(尽管它们在同一个目录下)。这表明实时防病毒扫描器已经启动,即使文件没有被打开只是进行了 fstat 操作。而使用 DIR 可以避免这种情况,因为它只访问目录信息。 - robert4

2

根据spookycoder的回答,我发现这种变体(使用DirectoryInfo)至少快了2倍(在复杂的文件夹结构上高达10倍!):

    public static long CalcDirSize(string sourceDir, bool recurse = true)
    {
        return _CalcDirSize(new DirectoryInfo(sourceDir), recurse);
    }

    private static long _CalcDirSize(DirectoryInfo di, bool recurse = true)
    {
        long size = 0;
        FileInfo[] fiEntries = di.GetFiles();
        foreach (var fiEntry in fiEntries)
        {
            Interlocked.Add(ref size, fiEntry.Length);
        }

        if (recurse)
        {
            DirectoryInfo[] diEntries = di.GetDirectories("*.*", SearchOption.TopDirectoryOnly);
            System.Threading.Tasks.Parallel.For<long>(0, diEntries.Length, () => 0, (i, loop, subtotal) =>
            {
                if ((diEntries[i].Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint) return 0;
                subtotal += __CalcDirSize(diEntries[i], true);
                return subtotal;
            },
                (x) => Interlocked.Add(ref size, x)
            );

        }
        return size;
    }

1

我认为它不会有太大的变化,但如果您使用API函数FindFirstFileNextFile来完成它,它可能会快一点。

然而,我认为没有真正快速的方法。为了比较,您可以尝试在Windows资源管理器中列出目录,或者使用dir /a /x /s > dirlist.txt命令,看看它们的速度如何,但我认为它们与FindFirstFile类似。

PInvoke提供了如何使用API的示例。


0

出于性能原因,我放弃了 .NET 实现,并使用本地函数 GetFileAttributesEx(...)。

试试这个:

[StructLayout(LayoutKind.Sequential)]
public struct WIN32_FILE_ATTRIBUTE_DATA
{
    public uint fileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME creationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME lastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME lastWriteTime;
    public uint fileSizeHigh;
    public uint fileSizeLow;
}

public enum GET_FILEEX_INFO_LEVELS
{
    GetFileExInfoStandard,
    GetFileExMaxInfoLevel
}

public class NativeMethods {
    [DllImport("KERNEL32.dll", CharSet = CharSet.Auto)]
    public static extern bool GetFileAttributesEx(string path, GET_FILEEX_INFO_LEVELS  level, out WIN32_FILE_ATTRIBUTE_DATA data);

}

现在只需执行以下操作:

WIN32_FILE_ATTRIBUTE_DATA data;
if(NativeMethods.GetFileAttributesEx("[your path]", GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, out data)) {

     long size = (data.fileSizeHigh << 32) & data.fileSizeLow;
}

我的电脑上无法运行。对于文件夹,文件大小高和文件大小低始终为零。 - AMissico
你试过用GET_FILEEX_INFO_LEVELS.GetFileMaxInfoLevel吗?另外路径结尾没有反斜杠吗? - Adrian Regan
我也无法运行。GetFileAttributesEx返回true,但fileSizeHigh和fileSizeLow始终为零。尝试了带和不带尾随斜杠的情况。 - Matt Tsōnto

0

如果有成千上万个文件,你不可能通过正面攻击获胜。你需要尝试更具创造性的解决方案。有这么多的文件,你甚至可能会发现,在计算大小的时间内,文件已经发生了变化,你的数据已经错误了。

所以,你需要将负载移动到其他地方。对我来说,答案是使用System.IO.FileSystemWatcher并编写一些代码来监视目录并更新索引。

编写一个Windows服务只需要很短的时间,可以配置为监视一组目录并将结果写入共享输出文件。您可以让服务在启动时重新计算文件大小,但然后只需在System.IO.FileSystemWatcher触发Create/Delete/Changed事件时监视更改即可。监视目录的好处是,您只关心小的更改,这意味着您的数字更有可能是正确的(记住所有数据都是陈旧的!)

然后,唯一需要注意的是,您将有多个资源都尝试访问生成的输出文件。因此,请确保考虑到这一点。


3
请不要这样做,否则会占用其它应用程序的资源。更不用说这种技巧非常脆弱。 - stuck

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