使用.NET提高枚举文件和文件夹的性能

时间:2013-07-19 21:47:49

标签: c# .net performance

我有一个包含数千个文件夹的基目录。在这些文件夹中,可以有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分钟,但仍然没有完成。我很好奇是否有人能够看到我可以提高上述线路的性能,或者如果有不同的方式我应该完全接近这个以获得更好的性能?建议?

7 个答案:

答案 0 :(得分:18)

这可能(可能)和它一样好:

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();

这里它将并行性移动到主文件夹枚举。上述大部分变化也适用。

答案 1 :(得分:16)

可能更快的替代方法是使用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;
}

修改

因此,根据以下评论,我决定在此处提供建议解决方案的基准以及我能想到的其他解决方案。有趣的是, EnumerateFiles 似乎比C#中的 FindNextFile更胜一筹,而带有EnumerateFiles AsParallel到目前为止命令提示计数令人惊讶的最快。但是请注意AsParallel没有得到完整的文件计数,或者缺少其他人计算的文件,所以你可以说命令提示方法是最好的

适用的配置:

  • Windows 7 Service Pack 1 x64
  • Intel(R)Core(TM)i5-3210M CPU @ 2.50GHz 2.50GHz
  • RAM:6GB
  • 平台目标:x64
  • 无优化(注意:使用优化进行编译会产生极差的性能)
  • 允许UnSafe代码
  • 启动而不调试

以下是三个屏幕截图:

Run 1

Run 2

Run 3

我在下面列出了我的测试代码:

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

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

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

答案 2 :(得分:2)

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

NTFS的库在这里https://github.com/LordMike/NtfsLib我还没有亲自使用https://discutils.codeplex.com/

我只会使用这些方法在x天之前初步发现文件,然后在删除之前验证它们是个体的,这可能有点矫枉过正,但我​​很谨慎。

答案 3 :(得分:0)

您使用的是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);
        }
    }            
}

答案 4 :(得分:0)

如果您真的想要提高性能,请尽量使用Windows内部的NtQueryDirectoryFile,并使用较大的缓冲区大小。

FindFirstFile已经很慢,虽然FindFirstFileEx稍好一些,但最好的效果将来自直接调用本机函数。

答案 5 :(得分:0)

上面的答案(#itsnotalie&amp; #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();
}

答案 6 :(得分:0)

使用SearchOption.AllDirectoriesEnumerateFiles花费了很长时间才返回第一项。在阅读了几个好的答案之后,我现在结束了下面的功能。通过一次只在一个目录上工作,然后递归调用它,现在几乎立即返回第一项。 但是我必须承认,我不确定使用.AsParallel()的正确方法,所以请不要盲目使用。

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

递归调用可能会影响堆栈,但是对于大多数FS来说,可以有多少个级别是有限制的,因此不应成为真正的问题。

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