【问题标题】:Improve the performance for enumerating files and folders using .NET提高使用 .NET 枚举文件和文件夹的性能
【发布时间】:2013-07-19 21:47:49
【问题描述】:

我有一个包含数千个文件夹的基本目录。在这些文件夹中,可以有 1 到 20 个子文件夹,其中包含 1 到 10 个文件。我想删除所有超过 60 天的文件。我正在使用下面的代码来获取我必须删除的文件列表:

DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
FileInfo[] oldFiles = 
  dirInfo.GetFiles("*.*", SearchOption.AllDirectories)
    .Where(t=>t.CreationTime < DateTime.Now.AddDays(-60)).ToArray();

但是我让它运行了大约 30 分钟,它仍然没有完成。我很好奇是否有人可以看到我可能会提高上述线路的性能,或者是否有不同的方式我应该完全接近这一点以获得更好的性能?有什么建议吗?

【问题讨论】:

  • 你可以考虑使用多线程来加快速度;但请确保他们没有查看相同的目录和内容。
  • @Steve,EnumerateFiles 已经运行了大约 5 分钟,不幸的是它仍在运行。
  • 对不起,我很困惑。你能解释一下你的意思吗?
  • @aevitas:多线程不太可能带来改进。限制因素是磁盘的速度,它一次只能做一件事。您的多个线程将花费大部分时间等待。
  • 看看内存消耗,有这么多文件和文件夹可能会很大。当您省略 ToArray() 并立即在循环中处理结果时,它不必将其全部构建在内存中。

标签: c# .net performance


【解决方案1】:

这(可能)和它会得到的一样好:

DateTime sixtyLess = DateTime.Now.AddDays(-60);
DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
FileInfo[] oldFiles = 
    dirInfo.EnumerateFiles("*.*", SearchOption.AllDirectories)
           .AsParallel()
           .Where(fi => fi.CreationTime < sixtyLess).ToArray();

变化:

  • 减少 60 天 DateTime 常量,从而减少 CPU 负载。
  • 使用EnumerateFiles
  • 使查询并行化。

应该在更短的时间内运行(不确定如何小得多)。

这是另一个可能比第一个更快或更慢的解决方案,这取决于数据:

DateTime sixtyLess = DateTime.Now.AddDays(-60);
DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
FileInfo[] oldFiles = 
     dirInfo.EnumerateDirectories()
            .AsParallel()
            .SelectMany(di => di.EnumerateFiles("*.*", SearchOption.AllDirectories)
                                .Where(fi => fi.CreationTime < sixtyLess))
            .ToArray();

在这里它将并行度移动到主文件夹枚举。上面的大部分更改也适用。

【讨论】:

  • 这里的问题是可能的访问异常。有解决办法吗?
【解决方案2】:

可能更快的替代方法是使用 WINAPI FindNextFile。有一个很好的Faster Directory Enumeration Tool。可以这样使用:

HashSet<FileData> GetPast60(string dir)
{
    DateTime retval = DateTime.Now.AddDays(-60);
    HashSet<FileData> oldFiles = new HashSet<FileData>();

    FileData [] files = FastDirectoryEnumerator.GetFiles(dir);
    for (int i=0; i<files.Length; i++)
    {
        if (files[i].LastWriteTime < retval)
        {
            oldFiles.Add(files[i]);
        }
    }    
    return oldFiles;
}

编辑

因此,基于下面的 cmets,我决定对这里的建议解决方案以及我能想到的其他解决方案进行基准测试。有趣的是,EnumerateFiles 似乎优于 FindNextFile in C#,而 EnumerateFilesAsParallel 是迄今为止最快的,令人惊讶的是命令提示符计数。但是请注意,AsParallel 没有获得完整的文件计数,或者丢失了其他文件计数的一些文件,因此您可以说命令提示符方法是最好的

适用配置:

  • Windows 7 Service Pack 1 x64
  • Intel(R) Core(TM) i5-3210M CPU @2.50GHz 2.50GHz
  • 内存:6GB
  • 平台目标:x64
  • 无优化(注意:使用优化编译会导致性能极差)
  • 允许不安全的代码
  • 不调试就开始

以下是三张截图:

我在下面包含了我的测试代码:

static void Main(string[] args)
{
    Console.Title = "File Enumeration Performance Comparison";
    Stopwatch watch = new Stopwatch();
    watch.Start();

    var allfiles = GetPast60("C:\\Users\\UserName\\Documents");
    watch.Stop();
    Console.WriteLine("Total time to enumerate using WINAPI =" + watch.ElapsedMilliseconds + "ms.");
    Console.WriteLine("File Count: " + allfiles);

    Stopwatch watch1 = new Stopwatch();
    watch1.Start();

    var allfiles1 = GetPast60Enum("C:\\Users\\UserName\\Documents\\");
    watch1.Stop();
    Console.WriteLine("Total time to enumerate using EnumerateFiles =" + watch1.ElapsedMilliseconds + "ms.");
    Console.WriteLine("File Count: " + allfiles1);

    Stopwatch watch2 = new Stopwatch();
    watch2.Start();

    var allfiles2 = Get1("C:\\Users\\UserName\\Documents\\");
    watch2.Stop();
    Console.WriteLine("Total time to enumerate using Get1 =" + watch2.ElapsedMilliseconds + "ms.");
    Console.WriteLine("File Count: " + allfiles2);


    Stopwatch watch3 = new Stopwatch();
    watch3.Start();

    var allfiles3 = Get2("C:\\Users\\UserName\\Documents\\");
    watch3.Stop();
    Console.WriteLine("Total time to enumerate using Get2 =" + watch3.ElapsedMilliseconds + "ms.");
    Console.WriteLine("File Count: " + allfiles3);

    Stopwatch watch4 = new Stopwatch();
    watch4.Start();

    var allfiles4 = RunCommand(@"dir /a: /b /s C:\Users\UserName\Documents");
    watch4.Stop();
    Console.WriteLine("Total time to enumerate using Command Prompt =" + watch4.ElapsedMilliseconds + "ms.");
    Console.WriteLine("File Count: " + allfiles4);


    Console.WriteLine("Press Any Key to Continue...");
    Console.ReadLine();
}

private static int RunCommand(string command)
{
    var process = new Process()
    {
        StartInfo = new ProcessStartInfo("cmd")
        {
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            CreateNoWindow = true,
            Arguments = String.Format("/c \"{0}\"", command),
        }
    };
    int count = 0;
    process.OutputDataReceived += delegate { count++; };
    process.Start();
    process.BeginOutputReadLine();

    process.WaitForExit();
    return count;
}

static int GetPast60Enum(string dir)
{
    return new DirectoryInfo(dir).EnumerateFiles("*.*", SearchOption.AllDirectories).Count();
}

private static int Get2(string myBaseDirectory)
{
    DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
    return dirInfo.EnumerateFiles("*.*", SearchOption.AllDirectories)
               .AsParallel().Count();
}

private static int Get1(string myBaseDirectory)
{
    DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
    return dirInfo.EnumerateDirectories()
               .AsParallel()
               .SelectMany(di => di.EnumerateFiles("*.*", SearchOption.AllDirectories))
               .Count() + dirInfo.EnumerateFiles("*.*", SearchOption.TopDirectoryOnly).Count();
}


private static int GetPast60(string dir)
{
    return FastDirectoryEnumerator.GetFiles(dir, "*.*", SearchOption.AllDirectories).Length;
}

注意:我专注于基准未修改日期的计数。

【讨论】:

  • 您可能想从那里使用 EnumerateFiles,但无论如何,它现在可能已使用 DirectoryInfo.EnumerateFiles 修复。
  • EnumerateFiles 并没有在该帖子中提及,
  • 好吧,原来我错了,Directory.EnumerateFiles 真的很神奇。
  • 这些测试应该重新运行,但使用下面的修复程序来计算根目录中的文件,请参阅@pisker stackoverflow.com/a/45121298/2716218的答案
【解决方案3】:

我意识到这对聚会来说已经很晚了,但是如果其他人正在寻找这个,那么您可以通过直接解析文件系统的 MFT 或 FAT 来加快速度,这需要管理员权限,我认为无论安全性如何,它都会返回所有文件,但至少在枚举阶段可能需要 30 分钟到 30 秒。

这里有一个用于 NTFS 的库 https://github.com/LordMike/NtfsLib 还有一个 https://discutils.codeplex.com/ 我没有亲自使用过。

我只会将这些方法用于最初发现超过 x 天的文件,然后在删除之前单独验证它们,这可能有点矫枉过正,但我​​很谨慎。

【讨论】:

    【解决方案4】:

    上述答案(#itsnotalie & #Chibueze Opata)中的Get1方法缺少计算根目录中的文件,所以它应该是:

    private static int Get1(string myBaseDirectory)
    {
        DirectoryInfo dirInfo = new DirectoryInfo(myBaseDirectory);
        return dirInfo.EnumerateDirectories()
                   .AsParallel()
                   .SelectMany(di => di.EnumerateFiles("*.*", SearchOption.AllDirectories))
                   .Count() + dirInfo.EnumerateFiles("*.*", SearchOption.TopDirectoryOnly).Count();
    }
    

    【讨论】:

      【解决方案5】:

      您正在使用 Linq。如果您编写自己的特殊情况下递归搜索目录的方法会更快。

      public static DateTime retval = DateTime.Now.AddDays(-60);
      
      public static void WalkDirectoryTree(System.IO.DirectoryInfo root)
      {
          System.IO.FileInfo[] files = null;
          System.IO.DirectoryInfo[] subDirs = null;
      
          // First, process all the files directly under this folder 
          try
          {
              files = root.GetFiles("*.*");
          }
          // This is thrown if even one of the files requires permissions greater 
          // than the application provides. 
          catch (UnauthorizedAccessException e)
          {
              // This code just writes out the message and continues to recurse. 
              // You may decide to do something different here. For example, you 
              // can try to elevate your privileges and access the file again.
              log.Add(e.Message);
          }
          catch (System.IO.DirectoryNotFoundException e)
          {
              Console.WriteLine(e.Message);
          }
      
          if (files != null)
          {
              foreach (System.IO.FileInfo fi in files)
              {
                if (fi.LastWriteTime < retval)
                {
                  oldFiles.Add(files[i]);
                }
      
                  Console.WriteLine(fi.FullName);
              }
      
              // Now find all the subdirectories under this directory.
              subDirs = root.GetDirectories();
      
              foreach (System.IO.DirectoryInfo dirInfo in subDirs)
              {
                  // Resursive call for each subdirectory.
                  WalkDirectoryTree(dirInfo);
              }
          }            
      }
      

      【讨论】:

      • 减速不是来自 LINQ。 LINQ 很慢,但除非您滥用或重置枚举器,否则它不会那么慢。
      【解决方案6】:

      如果您真的想要提高性能,请亲自动手并使用 Windows 内部的NtQueryDirectoryFile,它具有较大的缓冲区大小。

      FindFirstFile 已经很慢了,虽然FindFirstFileEx 稍微好一点,但最好的性能将来自直接调用本机函数。

      【讨论】:

        【解决方案7】:

        当使用 SearchOption.AllDirectories EnumerateFiles 需要很长时间才能返回第一个项目。在这里阅读了几个很好的答案后,我现在已经完成了以下功能。通过一次只在一个目录上工作并递归调用它,它现在几乎立即返回第一项。 但我必须承认,我并不完全确定使用.AsParallel() 的正确方法,所以不要盲目使用它。

        我强烈建议不要使用数组,而是使用枚举。 有人提到磁盘速度是限制因素,线程无济于事,就总时间而言,只要操作系统没有缓存任何内容,就很可能,但是通过使用多个线程,您可以获得首先返回的缓存数据,而否则可能会修剪缓存以为新结果腾出空间。

        递归调用可能会影响堆栈,但大多数 FS 对可以有多少层都有限制,因此不应成为真正的问题。

            private static IEnumerable<FileInfo> EnumerateFilesParallel(DirectoryInfo dir)
            {
                return dir.EnumerateDirectories()
                    .AsParallel()
                    .SelectMany(EnumerateFilesParallel)
                    .Concat(dir.EnumerateFiles("*", SearchOption.TopDirectoryOnly).AsParallel());
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-08-12
          • 2015-08-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多