¿Hay una forma más rápida de escanear un directorio recursivamente en .NET?

Estoy escribiendo un escáner de directorios en .NET.

Para cada archivo / directorio necesito la siguiente información.

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

Tengo esta función:

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

Resulta que esta implementación es bastante lenta. Hay alguna manera de acelerar esto? Estoy pensando en codificar esto a mano con FindFirstFileW pero me gustaría evitar eso si hay un camino integrado que sea más rápido.

Esta implementación, que necesita un poco de ajuste es 5-10 veces más 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 extensión:

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

Los valores de interoperabilidad son:

  [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 una larga historia de que los métodos de enumeración de archivos .NET son lentos. El problema es que no hay una forma instantánea de enumerar estructuras de directorios grandes. Incluso la respuesta aceptada aquí tiene sus problemas con las asignaciones de GC.

Lo mejor que he podido hacer está envuelto en mi biblioteca y expuesto como la clase FileFile ( fuente ) en el espacio de nombre CSharpTest.Net.IO . Esta clase puede enumerar archivos y carpetas sin asignaciones GC innecesarias y clasificación de cadenas.

El uso es lo suficientemente simple, y la propiedad RaiseOnAccessDenied omitirá los directorios y archivos a los que el usuario no tiene acceso:

  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 mi unidad local C: \ esto produce lo siguiente:

Enumeraron 810.046 archivos por un total de 307.707.792.662 bytes en 232.876 segundos.

Su kilometraje puede variar según la velocidad de la unidad, pero este es el método más rápido que he encontrado para enumerar archivos en código administrado. El parámetro de evento es una clase mutante de tipo FindFile.FileFoundEventArgs así que asegúrese de no mantener una referencia ya que los valores cambiarán para cada evento que se genere.

También puede observar que DateTime está expuesto solo en UTC. La razón es que la conversión a la hora local es semi-costosa. Puede considerar utilizar tiempos UTC para mejorar el rendimiento en lugar de convertirlos a la hora local.

Dependiendo de la cantidad de tiempo que esté tratando de reducir la función, puede valer la pena llamar directamente a las funciones de la API de Win32, ya que la API existente realiza un gran procesamiento para verificar cosas que pueden no interesarle.

Si aún no lo ha hecho y suponiendo que no tiene la intención de contribuir con el proyecto Mono, recomiendo descargar Reflector y ver cómo Microsoft implementó las llamadas API que está utilizando actualmente. Esto le dará una idea de lo que debe llamar y lo que puede omitir.

Podría, por ejemplo, optar por crear un iterador que yield nombres de directorio en lugar de una función que devuelva una lista, de esa manera no termine iterando sobre la misma lista de nombres dos o tres veces a través de los diversos niveles de código.

Es bastante superficial, 371 dirs con un promedio de 10 archivos en cada directorio. algunos directorios contienen otros subdirectores

Esto es solo un comentario, pero sus números parecen ser bastante altos. Ejecuté el siguiente a continuación, utilizando esencialmente el mismo método recursivo que está utilizando y mis tiempos son mucho más bajos a pesar de la creación de salida de cadena.

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

Ejecuté el código anterior en varios directorios diferentes. En mi máquina, la segunda llamada para escanear un árbol de directorios generalmente era más rápida debido al almacenamiento en caché, ya sea por el tiempo de ejecución o por el sistema de archivos. Tenga en cuenta que este sistema no es demasiado especial, solo una estación de trabajo de desarrollo de 1 año.

 // llamada en caché
 Dirs = 150, archivos = 420, profundidad máxima = 5
 Tiempo tomado = 53 milisegundos

 // llamada en caché
 Dirs = 1117, archivos = 9076, profundidad máxima = 11
 Tiempo tomado = 433 milisegundos

 // primera llamada
 Dirs = 1052, archivos = 5903, profundidad máxima = 12
 Tiempo tomado = 11921 milisegundos

 // primera llamada
 Dirs = 793, archivos = 10748, profundidad máxima = 10
 Tiempo transcurrido = 5433 milisegundos (2da ejecución 363 milisegundos)

Preocupado porque no estaba obteniendo la fecha de creación y modificación, el código fue modificado para mostrar esto también con los siguientes tiempos.

 // ahora agarrando la última actualización y la hora de creación.
 Dirs = 150, archivos = 420, profundidad máxima = 5
 Tiempo tomado = 103 milisegundos (2da ejecución 93 milisegundos)

 Dirs = 1117, archivos = 9076, profundidad máxima = 11
 Tiempo transcurrido = 992 milisegundos (2da ejecución 984 milisegundos)

 Dirs = 793, archivos = 10748, profundidad máxima = 10
 Tiempo tomado = 1382 milisegundos (2da ejecución 735 milisegundos)

 Dirs = 1052, archivos = 5903, profundidad máxima = 12
 Tiempo empleado = 936 milisegundos (2ª ejecución 595 milisegundos)

Nota: System.Diagnostics.StopWatch class se usa para el tiempo.

Acabo de encontrarme con esto. Buena implementación de la versión nativa.

Esta versión, aunque aún más lenta que la versión que usa FindFirst y FindNext , es bastante más rápida que la versión original de .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; } 

Debería producir el mismo resultado que su versión nativa. Mis pruebas muestran que esta versión demora alrededor de 1.7 veces más que la versión que usa FindFirst y FindNext . Tiempos obtenidos en modo de lanzamiento sin el depurador conectado.

Curiosamente, el cambio de GetFileSystemInfos a EnumerateFileSystemInfos agrega alrededor del 5% al ​​tiempo de ejecución en mis pruebas. Prefería que funcionara a la misma velocidad o posiblemente más rápido porque no tenía que crear la matriz de objetos FileSystemInfo .

El siguiente código es aún más corto porque permite que Framework se ocupe de la recursión. Pero es un 15% a 20% más lento que la versión anterior.

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

Nuevamente, si cambia eso a GetFileSystemInfos , será levemente (pero solo ligeramente) más rápido.

Para mis propósitos, la primera solución anterior es bastante rápida. La versión nativa se ejecuta en aproximadamente 1,6 segundos. La versión que usa DirectoryInfo ejecuta en aproximadamente 2.9 segundos. Supongo que si ejecutara estos escaneos con mucha frecuencia, cambiaría de opinión.

Usaría o me basaría en esta biblioteca multiproceso: http://www.codeproject.com/KB/files/FileFind.aspx

intente esto (es decir, haga la inicialización primero, y luego vuelva a usar su lista y sus objetos de directorio):

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

Recientemente tengo la misma pregunta, creo que también es bueno dar salida a todas las carpetas y archivos en un archivo de texto, y luego usar el lector de archivos para leer el archivo de texto, hacer lo que quiera procesar con múltiples hilos.

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

[actualización] Hola Moby, estás en lo cierto. Mi enfoque es más lento debido a la sobrecarga de leer el archivo de texto de salida. En realidad, me tomé un tiempo para probar la respuesta principal y cmd.exe con 2 millones de archivos.

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

El método de respuesta superior (53023) es más rápido que cmd.exe (64907), sin mencionar cómo mejorar la lectura del archivo de texto de salida. Aunque mi objective original es proporcionar una respuesta no demasiado mala, todavía siento lástima, ha.