2009-04-07 11 views
24

.NETでディレクトリスキャナを作成しています。.NETでディレクトリを再帰的にスキャンする方法はありますか?

各ファイル/ディレクトリについて、次の情報が必要です。

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

私はこの機能を持っている:

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

は、この実装はかなり遅いですが判明。これをスピードアップする方法はありますか?私はFindFirstFileWでこれをコーディングすることを考えていますが、速い方法でビルドされた方法があればそれを避けたいと思います。

+0

検索するファイル/ディレクトリの数はいくつですか?再帰の深さとは何ですか? –

+0

かなり浅く、各ディレクトリに平均10個のファイルがある371個のディレクトリです。いくつかのディレクトリには他のサブディレクトリがあります –

+1

P/Invokeのような感じです。あなたはまだスピードワーカーのスレッドを必要とする場合は助けることができます。 –

答えて

36

この実装は、微調整が必​​要ですが、5-10倍高速です。

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

拡張メソッド:

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は次のとおりです。

[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

OPが多くのファイルやフォルダをスキャンしているようです。基本的な問題は、管理されたコード自体ではなく、再帰的なディレクトリトラバーサルです。これは、5分の時間が30〜60秒に短縮されるため、これが機能する理由は、管理されていないため高速です。しかし、基本的な設計ミスはまだ「再帰的」です。これを「反復」ディレクトリトラバーサルにすると、Windowsに制限されることなく、これより4〜8倍高速(RecursiveScan2)の7秒間で管理コードを取得できます。 –

+0

@Quandary必ずしも真実ではありません。反復メソッドは、反復メソッド呼び出しごとにスタックへのメソッド情報のプッシュとポップのコストのために一般的に再帰メソッドは遅くなりますが、この場合の反復アプローチは、実行を続ける必要があるため残っている操作の数を集計します。たとえば、次を参照してください。http://stackoverflow.com/questions/26321366/fastest-way-to-get-directory-data-in-net – Alexandru

0

この(つまり、最初の初期化を行い、その後、あなたのリストとあなたの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

これは私のベンチマークで実際の違いはありません - あなたのメソッドは33156鉱山が33498を取る... interop oneは2872ミリ秒かかる... 10倍以上速く –

+0

これは私が最初に思ったことだ...しかし、私はテストし、それがほぼ同じ性能を持っていることに気付いた。 =/ – Lucas

+0

smbシェアで試してください –

5

を既存のAPIは興味のないものをチェックするために余分な処理を行うため、Win32 APIの関数を直接呼び出すことはできません。

まだ実行しておらず、 Monoプロジェクトに貢献するつもりですが、私はReflectorをダウンロードし、Microso ftは現在使用しているAPI呼び出しを実装しました。これは、あなたが何を呼び出す必要があるのか​​、何を外すことができるのかをあなたに知らせるでしょう。

例えば、リストを返す関数の代わりにyieldというディレクトリ名を持つイテレータを作成すると、同じリストの名前を2〜3回繰り返してしまうことはありませんさまざまなレベルのコード。

+0

モノはこれと何をしていますか? –

+2

@Cyril、http://mono-project.com/Contributingで、その要件について読むことができます。彼らは、「Microsoftの.NET実装や共有ソースコードを見れば、Monoに貢献することはできません」と明言しています。彼らはリフレクターについても言及しています。 – sisve

2

そのかなり浅い、371 dirs with 各ディレクトリの平均10ファイル いくつかのディレクトリには他のサブディレクトリがあります

これは単なるコメントですが、数字はかなり高いようです。基本的に同じ再帰メソッドを使用して以下を実行しました。文字列出力を作成しても、私の時間ははるかに短くなっています。

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

私は上記のコードをいくつかの異なるディレクトリで実行しました。私のマシンでは、ディレクトリツリーをスキャンする2回目の呼び出しは、ランタイムまたはファイルシステムのいずれかによってキャッシュされるため、通常は高速でした。このシステムは特別なものではなく、わずか1歳の開発ワークステーションであることに注意してください。

 
// 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) 

作成日時と変更日時が表示されていないことを心配し、次の時刻に出力するようにコードを修正しました。

 
// 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) 

注:System.Diagnostics.StopWatchクラスはタイミングに使用されます。

+0

yerpすべての番号はネットワーク共有をスキャンしたものです。ので、非常に高いと予想される –

+0

ええ、あなたのより遅いアクセス速度は今より意味をなさない。 –

2

私はちょうどこれを横断しました。ネイティブバージョンの素晴らしい実装。

このバージョンは、FindFirstFindNextを使用するバージョンよりもまだ遅いですが、元の.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; 
    } 

ネイティブバージョンと同じ出力を生成する必要があります。私のテストでは、このバージョンはFindFirstFindNextを使用するバージョンの約1.7倍の時間がかかります。デバッガを接続せずにリリースモードで実行した場合のタイミング。

奇妙なことに、GetFileSystemInfosEnumerateFileSystemInfosに変更すると、テストで約5%のランニングタイムが追加されます。私はむしろ、それがFileSystemInfoオブジェクトの配列を作成する必要がなかったので、同じスピードで実行するか、おそらくより速くなると予想しました。

次のコードは、Frameworkが再帰的に世話をすることができるため、まだまだ短くなっています。しかし、これは上記のバージョンよりも15〜20%遅いです。

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

、あなたがGetFileSystemInfosに、それは少し(しかし、少しだけ)速くなることを変更した場合。

私の目的のために、上記の最初の解決方法は十分に速いです。ネイティブバージョンは約1.6秒で実行されます。 DirectoryInfoを使用するバージョンは約2.9秒で実行されます。私はこれらのスキャンを頻繁に実行していたとしたら、私の心は変わります。

5

.NETファイルの列挙方法が遅いという長い歴史があります。問題は、大きなディレクトリ構造を列挙する瞬間的な方法がないことです。ここで受け入れられた回答でさえ、GC割り当てに関する問題があります。

私ができることは、私のライブラリーにラップされ、CSharpTest.Net.IO namespaceFileFilesource)クラスとして公開されています。このクラスは、不必要なGC割り当てや文字列マーシャリングを行わずにファイルとフォルダを列挙できます。

使用量は十分に簡単で、RaiseOnAccessDeniedプロパティはディレクトリをスキップすると、ユーザーがファイルへのアクセス権がありません。私の地元のCについて

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

を:\これは、次の出力ドライブ:

列挙された810,046のファイルは、合計307,707,792,662バイトで232.876秒で​​す。

あなたの走行距離はドライブの速度によって異なる場合がありますが、これはマネージコードでファイルを列挙するのが最も速い方法です。イベントパラメータはタイプFindFile.FileFoundEventArgsの変異クラスですので、イベントが発生するたびに値が変更されるため、参照を保持しないように注意してください。

また、公開されているDateTimeはUTCでのみ表示されることに注意してください。その理由は、現地時間への変換が半高価であるためです。これらをローカル時間に変換するのではなく、UTC時間を使用してパフォーマンスを向上させることを検討することもできます。

+0

素晴らしいもの!しかし、なぜ 'total'の代わりに' sizelock'のように 'Interlocked.Increment(ref total)'を使用していますか?スレッドセーフな方法で 'total'をインクリメントし、' size'をインクリメントするのはなぜですか? – mhu

+0

@mhu良い質問ですが、3年前に私は思い出したかもしれませんが、今私は完全に迷っています。コールバックはシングルスレッドであり、コールバックは必要ありません。 –

+0

それは私が思ったものです。返信してくれてありがとう。 – mhu

0

最近私は同じ質問をしています。すべてのフォルダとファイルをテキストファイルに出力してから、streamreaderを使ってテキストファイルを読み込み、マルチスレッドで処理したいものを実行することも良いと思います。

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

[更新] こんにちはモービーは、あなたが正しいです。 私のアプローチは、出力テキストファイルを読み戻すオーバーヘッドのために遅くなります。 実際には、トップアンサーとcmd.exeを200万のファイルでテストするのに時間がかかりました。

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

トップ解答方法(53023)は読ん出力テキストファイルを改善する方法はもちろんのこと、(64907)cmd.exeのよりも高速です。私の元々のポイントはあまりにも悪い答えを提供することですが、残念ながら、HA。

+0

これは、同時にテキストファイルに書き込んでいるので遅くなり、呼び出し元がそのテキストファイルを再度読み込んでから削除する必要があります。さらに、cmd.exe自体を実行するとオーバーヘッドが増加します。それは正しくエラーを処理しません、それが進むにつれ、フィードバックを与えることはできません... –

関連する問題