2009-04-07 25 views
24

Estoy escribiendo un escáner de directorios en .NET.¿Hay una manera más rápida de escanear un directorio recursivamente 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; 
    } 

que tienen esta función:

 static List<Info> RecursiveMovieFolderScan(string path){ 

     var info = new List<Info>(); 
     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 lento. 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.

+0

¿Cuántos archivos/directorios estás buscando? ¿Cuál es la profundidad de recursión? –

+0

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

+1

Parece que P/Invoke es el ganador aquí. Si aún necesita más velocidad, los hilos del trabajador podrían ayudar. –

Respuesta

36

Esta implementación, que necesita un poco de ajuste es 5-10 veces más rápida. Método

static List<Info> RecursiveScan2(string directory) { 
     IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); 
     WIN32_FIND_DATAW findData; 
     IntPtr findHandle = INVALID_HANDLE_VALUE; 

     var info = new List<Info>(); 
     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; 
    } 

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

defs 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; 
    } 
+2

Me parece que el OP está ejecutando un escaneo sobre muchos archivos y carpetas. El problema básico es el recorrido del directorio "recursivo", no el código administrado en sí. La razón por la que esto funciona es porque reduce un tiempo de 5 minutos a 30-60 segundos, porque no administrado, es más rápido. Pero el error de diseño básico sigue siendo "recursivo". Si lo convierte en un recorrido de directorios "iterativo", puede obtenerlo hasta 7 segundos en el código administrado, que todavía es 4-8 veces más rápido que este (RecursiveScan2), sin limitarlo a Windows. –

+0

@Quandary No siempre es cierto. Mientras que los métodos recursivos generalmente son más lentos debido al costo de empujar y extraer la información del método hacia y desde la pila para cada llamada de método iterativo, un enfoque iterativo en este caso sería casi igual de caro debido al costo adicional de tener que mantener un funcionamiento recuento del número de operaciones restantes, por ejemplo, consulte: http://stackoverflow.com/questions/26321366/fastest-way-to-get-directory-data-in-net – Alexandru

0

probar esto (es decir, hacer la inicialización en primer lugar, y luego volver a utilizar su lista y sus objetos DirectoryInfo):

static List<Info> RecursiveMovieFolderScan1() { 
     var info = new List<Info>(); 
     var dirInfo = new DirectoryInfo(path); 
     RecursiveMovieFolderScan(dirInfo, info); 
     return info; 
    } 

    static List<Info> RecursiveMovieFolderScan(DirectoryInfo dirInfo, List<Info> 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; 
} 
+0

Esto no hace una diferencia real en mi punto de referencia - su método toma 33156 mina toma 33498 ... la interopera toma 2872 milisegundos ... más de 10 veces más rápido –

+0

esto fue lo que primero pensé, también ... pero luego probé y noté que tenía casi el mismo rendimiento. =/ – Lucas

+0

pruébalo en una parte de smb –

5

Dependiendo de la cantidad de tiempo que está tratando de afeitar de la función, puede valer la pena Mientras tanto, llame directamente a las funciones de API de Win32, ya que la API existente realiza un gran procesamiento adicional para verificar cosas que pueden no interesarle.

Si aún no lo ha hecho y suponiendo que no lo hace intención de contribuir al proyecto Mono, recomiendo descargar Reflector y echar un vistazo a cómo Microso ft 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 s nombres de directorio en lugar de una función que devuelve una lista, de esa manera no termine iterando sobre la misma lista de nombres dos o tres veces a través de todos los diferentes niveles de código

+0

¿Qué tiene que ver Mono con esto ...? –

+2

@Cyril, en http://mono-project.com/Contributing puedes leer sobre sus requisitos. Explican explícitamente que "si ha observado la implementación de Microsoft de .NET o su código fuente compartido, no podrá contribuir a Mono". También mencionan el reflector. – sisve

2

Su bastante superficial, 371 directorios con 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(); 
     } 
    } 

Ejecuto el código anterior en una serie de 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.

 
// cached call 
Dirs = 150, files = 420, max depth = 5 
Time taken = 53 milliseconds 

// cached call 
Dirs = 1117, files = 9076, max depth = 11 
Time taken = 433 milliseconds 

// first call 
Dirs = 1052, files = 5903, max depth = 12 
Time taken = 11921 milliseconds 

// first call 
Dirs = 793, files = 10748, max depth = 10 
Time taken = 5433 milliseconds (2nd run 363 milliseconds) 

Preocupada de que yo no estaba recibiendo la creación y fecha de modificación, el código fue modificado para la producción de este, así como con las siguientes veces.

 
// now grabbing last update and creation time. 
Dirs = 150, files = 420, max depth = 5 
Time taken = 103 milliseconds (2nd run 93 milliseconds) 

Dirs = 1117, files = 9076, max depth = 11 
Time taken = 992 milliseconds (2nd run 984 milliseconds) 

Dirs = 793, files = 10748, max depth = 10 
Time taken = 1382 milliseconds (2nd run 735 milliseconds) 

Dirs = 1052, files = 5903, max depth = 12 
Time taken = 936 milliseconds (2nd run 595 milliseconds) 

Nota: System.Diagnostics.StopWatch class used for timing.

+0

yerp todos mis números provienen de la exploración de una red compartida . por lo que se espera que sea bastante alto –

+0

Sí, sus velocidades de acceso más lento tienen más sentido ahora. –

2

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

Esta versión, si bien es 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<Info> RecursiveMovieFolderScan(string path) 
    { 
     var info = new List<Info>(); 
     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; 
    } 

Debe producir el mismo resultado que su versión original. Mi prueba muestra que esta versión lleva aproximadamente 1.7 veces más tiempo que la versión que usa FindFirst y FindNext. Tiempos obtenidos en modo de lanzamiento sin el depurador conectado.

Curiosamente, cambiar GetFileSystemInfos a EnumerateFileSystemInfos agrega aproximadamente 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<Info> RecursiveScan3(string path) 
    { 
     var info = new List<Info>(); 

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

Una vez más, si cambia a que GetFileSystemInfos, que será un poco (pero sólo 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 se ejecuta en aproximadamente 2.9 segundos. Supongo que si ejecutara estos escaneos con mucha frecuencia, cambiaría de opinión.

5

Hay 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 (source) en el CSharpTest.Net.IO namespace. Esta clase puede enumerar archivos y carpetas sin asignaciones GC innecesarias y clasificación de cadenas.

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

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

Por mi local C: \ impulsar este emite la siguiente:

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

Su rendimiento 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 del 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 es posible que tenga en cuenta que los DateTime expuestos solo están 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.

+0

¡Cosas geniales! Pero ¿por qué estás usando 'Interlocked.Increment (ref total)' en lugar de 'total ++' como lo haces con 'size'. ¿Por qué incrementar 'total' en un hilo de manera segura y' size' no? – mhu

+0

@mhu buena pregunta, hace 3 años que podría haber recordado, pero en este momento estoy en una pérdida completa. La devolución de llamada tiene un solo hilo y no es necesario. –

+0

Eso es lo que pensé. Gracias por la respuesta. – mhu

0

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, que son correctos. 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 superior 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), por no hablar de cómo mejorar la lectura de archivos de texto de salida. Aunque mi objetivo original es proporcionar una respuesta no demasiado mala, todavía siento lástima, ha.

+0

Eso sería más lento ya que está escribiendo en un archivo de texto al mismo tiempo, luego requiere que la persona que llama vuelva a leer ese archivo de texto y luego lo elimine. Además, ejecutar cmd.exe en sí mismo agrega sobrecarga. No manejará los errores adecuadamente, no puede dar retroalimentación a medida que avanza, ... –

Cuestiones relacionadas