web-dev-qa-db-ja.com

.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で手動でコーディングすることを考えていますが、より高速な組み込みの方法がある場合は、それを避けたいと思います。

27
Sam Saffron

少し調整が必要なこの実装は、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);
        }
    }

相互運用機能の定義は次のとおりです。

    [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;
    }
39
Sam Saffron

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

私ができる最善のことは、ライブラリにラップされ、 FileFilesourceCSharpTest.Net.IO名前空間 のクラス。このクラスは、不要なGC割り当てや文字列マーシャリングなしでファイルとフォルダーを列挙できます。

使用法は非常に簡単で、RaiseOnAccessDeniedプロパティは、ユーザーがアクセスできないディレクトリとファイルをスキップします。

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

私のローカルC:\ドライブの場合、これは次のように出力します。

232.876秒で​​合計307,707,792,662バイトの810,046ファイルを列挙しました。

マイレージはドライブ速度によって異なる場合がありますが、これはマネージコード内のファイルを列挙するために私が見つけた最速の方法です。イベントパラメータはタイプ FindFile.FileFoundEventArgs の変更クラスであるため、発生するイベントごとに値が変わるため、参照を保持しないでください。 。

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

7
csharptest.net

関数を削り取ろうとしている時間によっては、Win32 API関数を直接呼び出す価値がある場合があります。これは、既存のAPIが、関心のないものをチェックするために多くの追加処理を行うためです。

まだ行っていない場合で、Monoプロジェクトに貢献するつもりがないと仮定すると、 Reflector をダウンロードして、MicrosoftがどのようにAPI呼び出しを実装したかを確認することを強くお勧めします。現在使用しています。これにより、何を呼び出す必要があり、何を省略できるかがわかります。

たとえば、リストを返す関数の代わりにyieldsディレクトリ名を指定するイテレータを作成することを選択できます。そうすれば、同じ名前のリストをすべて2〜3回繰り返す必要がなくなります。さまざまなレベルのコード。

5
tylerl

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

このバージョンは、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オブジェクトの配列を作成する必要がなかったため、同じ速度またはおそらくより高速で実行されることを期待していました。

次のコードは、フレームワークが再帰を処理できるようにするため、さらに短くなります。ただし、上記のバージョンよりも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秒で実行されます。これらのスキャンを頻繁に実行しているとしたら、気が変わったと思います。

3
Jim Mischel

そのかなり浅い、各ディレクトリに平均10個のファイルがある371個のdir。一部のdirには他のsubdirが含まれています

これは単なるコメントですが、あなたの数はかなり多いようです。私はあなたが使用しているのと本質的に同じ再帰的方法を使用して以下を実行しました、そして私の時間は文字列出力を作成したにもかかわらずはるかに短いです。

    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年前の開発ワークステーションであることに注意してください。

 //キャッシュされた呼び出し
 Dirs = 150、ファイル= 420、最大深度= 5 
所要時間= 53ミリ秒
 
 //キャッシュされた呼び出し
 Dirs = 1117、files = 9076、最大深度= 11 
所要時間= 433ミリ秒
 
 //最初の呼び出し
 Dirs = 1052、ファイル= 5903、最大深度= 12 
所要時間= 11921ミリ秒
 
 //最初の呼び出し
ディレクトリ= 793、ファイル= 10748、最大深度= 10 
所要時間= 5433ミリ秒(2回目の実行363ミリ秒)

作成日と変更日がわからないのではないかと心配して、以下の時間でこれも出力するようにコードを変更しました。

 //最後の更新と作成時間を取得しています。
 Dirs = 150、files = 420、最大深度= 5 
所要時間= 103ミリ秒(2回目の実行93ミリ秒)
 
 Dirs = 1117、files = 9076、最大深度= 11 
所要時間= 992ミリ秒(2回目の実行984ミリ秒)
 
 Dirs = 793、ファイル= 10748、最大深度= 10 
所要時間= 1382ミリ秒(2回目の実行735ミリ秒)
 
 Dirs = 1052、ファイル= 5903、最大深度= 12 
所要時間= 936ミリ秒(2回目の実行595ミリ秒)

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

2
Robert Paulson

私はこのマルチスレッドライブラリを使用またはベースにします: http://www.codeproject.com/KB/files/FileFind.aspx

1
Bertvan

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

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

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

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

トップアンサーメソッド(53023)は、出力テキストファイルの読み取りを改善する方法は言うまでもなく、cmd.exe(64907)よりも高速です。私の最初のポイントはそれほど悪くない答えを提供することですが、それでも申し訳ありません、ハ。

0
user3918709

これを試してください(つまり、最初に初期化を実行してから、リストと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
Jimmy