Повысьте производительность для перечисления файлов и папок с помощью .NET.

У меня есть базовый каталог, содержащий несколько тысяч папок. Внутри этих папок может находиться от 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 минут, и он все еще не закончен. Мне любопытно, может ли кто-нибудь увидеть, что я могу потенциально улучшить производительность вышеуказанной строки, или если есть другой способ, я должен подходить к этому целиком для лучшей производительности? Предложения?

Ответ 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 и, следовательно, уменьшил нагрузку на процессор.
  • Используется 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();

Здесь он перемещает parallelism в перечисление главной папки. Большинство изменений сверху также применяются.

Ответ 2

Возможно, более быстрой альтернативой является использование WINAPI FindNextFile. Для этого есть отличный инструмент для ускорения работы с каталогами. Который может быть использован следующим образом:

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

EDIT

Таким образом, основываясь на комментариях ниже, я решил сделать эталон предлагаемых решений здесь, а также другие, которые я мог придумать. Было достаточно интересно увидеть, что EnumerateFiles, кажется, превосходил FindNextFile в С#, в то время как EnumerateFiles с AsParallel был самым быстрым, за ним неожиданно последовала командная строка подсчитывать. Однако обратите внимание, что AsParallel не получил полное количество файлов или пропустил некоторые файлы, подсчитанные другими, поэтому вы можете сказать, что метод командной строки - лучший.

Применимая конфигурация:

  • Windows 7 с пакетом обновления 1 x64
  • Процессор Intel (R) Core (TM) i5-3210M @2,50 ГГц, 2,50 ГГц
  • RAM: 6 ГБ
  • Платформа Цель: x64
  • Без оптимизации (примечание: компиляция с оптимизацией приведет к крайне низкой производительности)
  • Разрешить небезопасный код
  • Начать без отладки

Ниже приведены три скриншота:

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() + dirInfo.EnumerateFiles("*.*", SearchOption.TopDirectoryOnly).Count();
}


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

NB. Я сконцентрировался на подсчете в контрольной точке без изменений

Ответ 3

Я понимаю, что это очень поздно для вечеринки, но если кто-то ищет это, то вы можете ускорить работу на порядок, просто разобрав MFT или FAT файловой системы, для этого нужны привилегии администратора, как я думаю он будет возвращать все файлы независимо от безопасности, но, возможно, займет 30 минут до 30 секунд для этапа перечисления, по крайней мере.

Библиотека для NTFS находится здесь https://github.com/LordMike/NtfsLib есть также https://discutils.codeplex.com/, который я лично не использовал.

Я бы использовал эти методы только для первоначального обнаружения файлов в течение x дней, а затем проверял их отдельно перед удалением, это может быть чрезмерным, но я так осторожен.

Ответ 4

Метод Get1 в приведенном выше ответе (#itsnotalie и #Chibueze Opata) отсутствует для подсчета файлов в корневом каталоге, поэтому он должен читать:

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

Ответ 6

Если вы действительно хотите повысить производительность, замарайте руки и используйте NtQueryDirectoryFile, который является внутренним для Windows, с большим размером буфера.

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