Existe uma maneira mais rápida de verificar através de um diretório recursivamente no .net?

Eu estou escrevendo um scanner de diretório no .net.

Para cada arquivo / diretório, preciso das informações a seguir.

class Info { public bool IsDirectory; public string Path; public DateTime ModifiedDate; public DateTime CreatedDate; } 

Eu tenho essa function:

  static List RecursiveMovieFolderScan(string path){ var info = new List(); var dirInfo = new DirectoryInfo(path); foreach (var dir in dirInfo.GetDirectories()) { info.Add(new Info() { IsDirectory = true, CreatedDate = dir.CreationTimeUtc, ModifiedDate = dir.LastWriteTimeUtc, Path = dir.FullName }); info.AddRange(RecursiveMovieFolderScan(dir.FullName)); } foreach (var file in dirInfo.GetFiles()) { info.Add(new Info() { IsDirectory = false, CreatedDate = file.CreationTimeUtc, ModifiedDate = file.LastWriteTimeUtc, Path = file.FullName }); } return info; } 

Acontece que esta implementação é bastante lenta. Existe alguma maneira de acelerar isso? Estou pensando em codificar manualmente isso com FindFirstFileW, mas gostaria de evitar que, se houver um construído de maneira que seja mais rápido.

Essa implementação, que precisa de alguns ajustes, é de 5 a 10 vezes mais rápida.

  static List RecursiveScan2(string directory) { IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); WIN32_FIND_DATAW findData; IntPtr findHandle = INVALID_HANDLE_VALUE; var info = new List(); try { findHandle = FindFirstFileW(directory + @"\*", out findData); if (findHandle != INVALID_HANDLE_VALUE) { do { if (findData.cFileName == "." || findData.cFileName == "..") continue; string fullpath = directory + (directory.EndsWith("\\") ? "" : "\\") + findData.cFileName; bool isDir = false; if ((findData.dwFileAttributes & FileAttributes.Directory) != 0) { isDir = true; info.AddRange(RecursiveScan2(fullpath)); } info.Add(new Info() { CreatedDate = findData.ftCreationTime.ToDateTime(), ModifiedDate = findData.ftLastWriteTime.ToDateTime(), IsDirectory = isDir, Path = fullpath }); } while (FindNextFile(findHandle, out findData)); } } finally { if (findHandle != INVALID_HANDLE_VALUE) FindClose(findHandle); } return info; } 

método de extensão:

  public static class FILETIMEExtensions { public static DateTime ToDateTime(this System.Runtime.InteropServices.ComTypes.FILETIME filetime ) { long highBits = filetime.dwHighDateTime; highBits = highBits << 32; return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime); } } 

defs de interoperabilidade são:

  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr FindFirstFileW(string lpFileName, out WIN32_FIND_DATAW lpFindFileData); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATAW lpFindFileData); [DllImport("kernel32.dll")] public static extern bool FindClose(IntPtr hFindFile); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WIN32_FIND_DATAW { public FileAttributes dwFileAttributes; internal System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; internal System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; public int nFileSizeHigh; public int nFileSizeLow; public int dwReserved0; public int dwReserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string cFileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName; } 

Existe uma longa história de que os methods de enumeração de arquivos .NET estão lentos. A questão é que não há uma maneira instantânea de enumerar estruturas de diretórios grandes. Até a resposta aceita aqui tem problemas com alocações de GC.

O melhor que eu consegui fazer foi embrulhado em minha biblioteca e exposto como a class FileFile ( fonte ) no namespace CSharpTest.Net.IO . Esta class pode enumerar arquivos e pastas sem alocações de GC desnecessárias e empacotamento de seqüência de caracteres.

O uso é bastante simples, e a propriedade RaiseOnAccessDenied irá ignorar os diretórios e arquivos aos quais o usuário não tem access:

  private static long SizeOf(string directory) { var fcounter = new CSharpTest.Net.IO.FindFile(directory, "*", true, true, true); fcounter.RaiseOnAccessDenied = false; long size = 0, total = 0; fcounter.FileFound += (o, e) => { if (!e.IsDirectory) { Interlocked.Increment(ref total); size += e.Length; } }; Stopwatch sw = Stopwatch.StartNew(); fcounter.Find(); Console.WriteLine("Enumerated {0:n0} files totaling {1:n0} bytes in {2:n3} seconds.", total, size, sw.Elapsed.TotalSeconds); return size; } 

Para minha unidade C: \ local, isso gera o seguinte:

Enumerados 810.046 arquivos totalizando 307.707.792.662 bytes em 232.876 segundos.

Sua milhagem pode variar de acordo com a velocidade da unidade, mas esse é o método mais rápido que já encontrei de enumerar arquivos no código gerenciado. O parâmetro event é uma class mutante do tipo FindFile.FileFoundEventArgs, portanto, certifique-se de não manter uma referência a ele, pois seus valores serão alterados para cada evento gerado.

Você também pode observar que os DateTime expostos são apenas em UTC. A razão é que a conversão para a hora local é semi-cara. Você pode considerar o uso de horários UTC para melhorar o desempenho em vez de convertê-los em horário local.

Dependendo de quanto tempo você está tentando raspar a function, pode valer a pena chamar diretamente as funções da API do Win32, já que a API existente faz muito processamento extra para verificar as coisas que talvez você não esteja interessado.

Se você ainda não fez isso, e supondo que não pretende contribuir para o projeto Mono, eu recomendo fortemente o download do Reflector e ver como a Microsoft implementou as chamadas da API que você está usando atualmente. Isso lhe dará uma idéia do que você precisa ligar e do que pode deixar de fora.

Você pode, por exemplo, optar por criar um iterador que yield nomes de diretórios s em vez de uma function que retorne uma lista, assim você não termina iterando sobre a mesma lista de nomes duas ou três vezes em todos os vários níveis de código.

É bem superficial, 371 dirs com média de 10 arquivos em cada diretório. alguns dirs contêm outros subdiretórios

Este é apenas um comentário, mas seus números parecem ser bem altos. Eu corri o abaixo usando essencialmente o mesmo método recursivo que você está usando e meus tempos são muito menores, apesar de criar saída de seqüência de caracteres.

  public void RecurseTest(DirectoryInfo dirInfo, StringBuilder sb, int depth) { _dirCounter++; if (depth > _maxDepth) _maxDepth = depth; var array = dirInfo.GetFileSystemInfos(); foreach (var item in array) { sb.Append(item.FullName); if (item is DirectoryInfo) { sb.Append(" (D)"); sb.AppendLine(); RecurseTest(item as DirectoryInfo, sb, depth+1); } else { _fileCounter++; } sb.AppendLine(); } } 

Eu corri o código acima em vários diretórios diferentes. Na minha máquina, a segunda chamada para varrer uma tree de diretórios geralmente era mais rápida devido ao armazenamento em cache, tanto pelo tempo de execução quanto pelo sistema de arquivos. Note que este sistema não é nada muito especial, apenas uma workstation de desenvolvimento de 1 ano.

 // chamada em cache
 Dirs = 150, arquivos = 420, profundidade máxima = 5
 Tempo gasto = 53 milissegundos

 // chamada em cache
 Dirs = 1117, arquivos = 9076, profundidade máxima = 11
 Tempo gasto = 433 milissegundos

 // primeira chamada
 Dirs = 1052, arquivos = 5903, profundidade máxima = 12
 Tempo gasto = 11921 milissegundos

 // primeira chamada
 Dirs = 793, arquivos = 10748, profundidade máxima = 10
 Tempo gasto = 5433 milissegundos (segunda execução, 363 milissegundos)

Preocupado que eu não estava recebendo a data de criação e modificação, o código foi modificado para produzir isso também com os seguintes horários.

 // agora pegando a última atualização e hora de criação.
 Dirs = 150, arquivos = 420, profundidade máxima = 5
 Tempo gasto = 103 milissegundos (2ª execução 93 milissegundos)

 Dirs = 1117, arquivos = 9076, profundidade máxima = 11
 Tempo gasto = 992 milissegundos (segunda execução, 984 milissegundos)

 Dirs = 793, arquivos = 10748, profundidade máxima = 10
 Tempo gasto = 1382 milissegundos (segunda execução, 735 milissegundos)

 Dirs = 1052, arquivos = 5903, profundidade máxima = 12
 Tempo gasto = 936 milissegundos (2ª execução 595 milissegundos)

Nota: Classe System.Diagnostics.StopWatch usada para o tempo.

Eu acabei de passar por isso. Boa implementação da versão nativa.

Esta versão, embora ainda mais lenta que a versão que usa FindFirst e FindNext , é um pouco mais rápida do que a versão original do .NET.

  static List RecursiveMovieFolderScan(string path) { var info = new List(); var dirInfo = new DirectoryInfo(path); foreach (var entry in dirInfo.GetFileSystemInfos()) { bool isDir = (entry.Attributes & FileAttributes.Directory) != 0; if (isDir) { info.AddRange(RecursiveMovieFolderScan(entry.FullName)); } info.Add(new Info() { IsDirectory = isDir, CreatedDate = entry.CreationTimeUtc, ModifiedDate = entry.LastWriteTimeUtc, Path = entry.FullName }); } return info; } 

Deve produzir a mesma saída que sua versão nativa. Meu teste mostra que esta versão leva cerca de 1,7 vezes a versão que usa FindFirst e FindNext . Timings obtidos no modo de liberação em execução sem o depurador anexado.

Curiosamente, alterando o GetFileSystemInfos para EnumerateFileSystemInfos adiciona cerca de 5% ao tempo de execução em meus testes. Eu esperava que ele fosse executado na mesma velocidade ou possivelmente mais rápido porque não precisava criar a matriz de objects FileSystemInfo .

O código a seguir é ainda mais curto, porque permite que o Framework cuide da recursion. Mas é um bom 15% a 20% mais lento que a versão acima.

  static List RecursiveScan3(string path) { var info = new List(); var dirInfo = new DirectoryInfo(path); foreach (var entry in dirInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) { info.Add(new Info() { IsDirectory = (entry.Attributes & FileAttributes.Directory) != 0, CreatedDate = entry.CreationTimeUtc, ModifiedDate = entry.LastWriteTimeUtc, Path = entry.FullName }); } return info; } 

Novamente, se você alterar isso para GetFileSystemInfos , ele será um pouco (mas apenas um pouco) mais rápido.

Para os meus propósitos, a primeira solução acima é bastante rápida. A versão nativa é executada em cerca de 1,6 segundos. A versão que usa DirectoryInfo é executada em cerca de 2,9 segundos. Eu suponho que se eu estivesse executando essas varreduras com muita frequência, eu mudaria de ideia.

Eu usaria ou basear-me nesta biblioteca multi-threaded: http://www.codeproject.com/KB/files/FileFind.aspx

tente isto (isto é, faça a boot primeiro e depois reutilize sua lista e seus objects directoryInfo):

  static List RecursiveMovieFolderScan1() { var info = new List(); var dirInfo = new DirectoryInfo(path); RecursiveMovieFolderScan(dirInfo, info); return info; } static List RecursiveMovieFolderScan(DirectoryInfo dirInfo, List info){ foreach (var dir in dirInfo.GetDirectories()) { info.Add(new Info() { IsDirectory = true, CreatedDate = dir.CreationTimeUtc, ModifiedDate = dir.LastWriteTimeUtc, Path = dir.FullName }); RecursiveMovieFolderScan(dir, info); } foreach (var file in dirInfo.GetFiles()) { info.Add(new Info() { IsDirectory = false, CreatedDate = file.CreationTimeUtc, ModifiedDate = file.LastWriteTimeUtc, Path = file.FullName }); } return info; } 

Recentemente eu tenho a mesma pergunta, eu acho que também é bom para a saída de todas as pastas e arquivos em um arquivo de texto e, em seguida, usar o leitor de texto para ler o arquivo de texto, faça o que você deseja processar com multi-thread.

 cmd.exe /u /c dir "M:\" /s /b >"c:\flist1.txt" 

[atualização] Oi Moby, você está correto. Minha abordagem é mais lenta devido à sobrecarga de leitura do arquivo de texto de saída. Na verdade eu levei algum tempo para testar a resposta principal e cmd.exe com 2 milhões de arquivos.

 The top answer: 2010100 files, time: 53023 cmd.exe method: 2010100 files, cmd time: 64907, scan output file time: 19832. 

O método de resposta principal (53023) é mais rápido que cmd.exe (64907), sem mencionar como melhorar a leitura do arquivo de texto de saída. Embora o meu ponto original é fornecer uma resposta não muito ruim, ainda sinto muito, ha.