【问题标题】:Directory file size calculation - how to make it faster?目录文件大小计算 - 如何使其更快?
【发布时间】:2011-02-28 02:18:31
【问题描述】:

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

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

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

有什么办法可以缩短我第一次运行程序所用的时间??

【问题讨论】:

  • 你的方法比“explorer”第一次做这个要慢吗?
  • 我认为这很正常。您可能会使用较低级别的 API 在文件系统级别进行递归,但我怀疑这会明显更快。
  • @Marc,不,没有太大的不同。另外,我尝试过 WinApi,但差别不大。
  • 使用文件夹分组选项对文件系统进行碎片整理将加快初始搜索速度; AFAIK,不,没有加速方法;您可以使用 SSD 驱动器...
  • @MarcGravell 你能在你的机器上做这个,让我知道它是否有效吗?

标签: c# windows winforms winapi filesystemobject


【解决方案1】:

如果摆弄了一段时间,尝试并行化它,并且令人惊讶的是 - 它在我的机器上加速(在四核上最多 3 倍),不知道它是否在所有情况下都有效,但给它试一试……

.NET4.0 代码(或使用 3.5 和 TaskParallelLibrary)

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

【讨论】:

  • 至少它可能优化了用户模式的操作。
  • 当我参加 Microsoft Visual Studio 2010 发布活动(英国技术日)时,用于演示新的 Parallel LINQ 方法的示例正是:计算目录大小。 IIRC 我们看到在他的四核笔记本电脑上使用 PLINQ 时速度至少提高了 2 倍。它在这里的一个视频中,但我不记得是哪一个:microsoft.com/uk/techdays/resources.aspx
  • 能否请您解释一下为什么要检查 ReparsePoint?因为如果我评论说线速度增加了 5 倍以上。
  • @AFgone 因为在我看来,重解析点不是真正的文件。 MSDN:“该文件包含一个重解析点,它是与文件或目录关联的用户定义数据块。”。但与往常一样,这取决于您的需求和要求。
【解决方案2】:

硬盘是一种有趣的野兽——顺序访问(例如读取一个大的连续文件)非常快,大约 80 兆字节/秒。但是随机访问非常慢。这就是您遇到的问题 - 递归到文件夹不会读取太多(就数量而言)数据,但需要很多随机读取。您第二次看到 zippy perf 的原因是因为 MFT 仍在 RAM 中(您对缓存的想法是正确的)

我见过的实现此目的的最佳机制是自己扫描 MFT。这个想法是您在一次线性传递中读取和解析 MFT,构建您需要的信息。最终结果将是在非常完整的 HD 上接近 15 秒。

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

FWIW:这种方法非常复杂,因为在 Windows(或我知道的任何操作系统)中确实没有很好的方法来执行此操作 - 问题在于确定需要哪些文件夹/文件的行为需要在磁盘上进行大量磁头移动。微软很难为你描述的问题建立一个通用的解决方案。

【讨论】:

    【解决方案3】:

    简短的回答是否定的。 Windows 可以使目录大小计算更快的方法是在每次文件写入时更新目录大小和所有父目录大小。但是,这会使文件写入操作变慢。由于文件写入比读取目录大小更常见,因此这是一个合理的权衡。

    我不确定正在解决什么问题,但如果是文件系统监控,可能值得一试:http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx

    【讨论】:

      【解决方案4】:

      在扫描包含数万个文件的文件夹时,使用任何方法性能都会受到影响。

      • 使用 Windows API FindFirstFile... 和 FindNextFile... 函数提供最快的访问。

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

      • 您如何处理任何文件访问方法的结果决定了您的应用程序的性能。例如,即使您使用 Windows API 函数,更新列表框也会影响性能。

      • 您无法将执行速度与 Windows 资源管理器进行比较。根据我的实验,我相信 Windows 资源管理器在很多情况下会直接从文件分配表中读取。

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

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

      建议

      • 我想知道BackupRead 对您的情况是否有帮助?

      • 如果您使用 DIR 并捕获然后解析其输出会怎样? (您并没有真正解析,因为每个 DIR 行都是固定宽度的,因此只需调用子字符串即可。)

      • 如果你在后台线程上调用DIR /B &gt; NULL 然后运行你的程序怎么办?在 DIR 运行时,您将受益于缓存文件访问。

      【讨论】:

      • 这是不正确的。 DIR 不从文件分配表中读取。 Windows 资源管理器也没有。两者都通过 Kernel32 和 NTDLL 进行调用,并由内核模式下的文件系统驱动程序处理。我在 cmd.exe 上运行了依赖遍历器 (depends.exe),并确定 DIR 命令调用了 Kernel32.dll 例程 FindFirstFileW 和 FindNextFileW。因此,直接调用 DIR 命令会比自己调用这些命令要慢。
      • 首先,不能使用“depends”来确定DIR命令使用什么API调用。
      • 其次,如果您使用“进程监视器”监视 DIR 命令,您会注意到只执行 QueryDirectory 操作。如果您在 .NET 中创建一个调用 GetFileSystemInfosGetDirectories 的简单控制台应用程序,您会注意到执行相同操作的频率更高,包括大量 CloseFileCreateFile 操作。这些 .NET 方法调用 API 例程。因此,您可以推断 DIR 命令没有调用这些 API 函数。
      • 第三,做我该做的。使用 C/C++ 创建控制台应用程序。此应用程序仅调用 API 例程并向下递归文件夹结构。它不输出任何内容。将其执行时间与重定向到 NULL 或文件的相同 DIR 命令进行比较。 DIR 命令总是明显更快。所有访问都必须通过文件系统驱动程序,但 DIR 和某些情况下的 Windows Explorer 直接从文件分配表中读取。请参阅 Chris Gray 的回答。
      • 最后,如果你真的想反驳直接从“胖子”读取的 DIR,请使用 DEBUG 并调试 CMD。我选择编写测试应用程序来验证我遇到的行为。我认为,DIR 有某种“钩子”,允许它读取“块”中的文件分配表。 (很可能它使用了 Chris Gray 的回答中的技术。)对于它能够如此快速地从硬盘读取文件信息的能力,没有其他解释。
      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

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

        但是,我认为没有任何真正快速的方法可以做到这一点。出于比较的目的,您可以尝试使用 dir /a /x /s &gt; dirlist.txt 并在 Windows 资源管理器中列出目录以查看它们的速度,但我认为它们将类似于 FindFirstFile

        PInvoke 有一个如何使用 API 的示例。

        【讨论】:

          【解决方案7】:

          拥有数以万计的文件,正面攻击是无法取胜的。您需要尝试在解决方案上更具创意。有了这么多文件,您甚至可能会发现,在您计算大小所需的时间里,文件已经更改,您的数据已经错误。

          因此,您需要将负载移至其他地方。对我来说,答案是使用System.IO.FileSystemWatcher 并编写一些代码来监控目录并更新索引。

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

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

          【讨论】:

          • 请不要这样做,您最终会占用所有其他应用程序的资源。更何况这个技巧很脆弱。
          【解决方案8】:

          我放弃了 .NET 实现(出于性能原因)并使用了 Native 函数 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;
          }
          

          【讨论】:

          • 不能在我的机器上工作。文件夹的 File-size-high 和 file-size-low 始终为零。
          • 你用 GET_FILEEX_INFO_LEVELS.GetFileMaxInfoLevel 试过了吗?路径末尾也没有尾随'\'?
          • 对我也不起作用。 GetFileAttributesEx 返回 true,但 fileSizeHigh 和 fileSizeLow 始终为零。尝试使用和不使用斜杠。
          猜你喜欢
          • 2013-01-31
          • 1970-01-01
          • 1970-01-01
          • 2014-05-16
          • 1970-01-01
          • 2013-05-25
          • 2011-03-11
          • 2010-11-26
          相关资源
          最近更新 更多