2009-04-07 29 views
24

Tôi đang viết một máy quét thư mục trong .NET.Có cách nào nhanh hơn để quét qua thư mục đệ quy trong .NET không?

Đối với mỗi Tệp/Dir, tôi cần thông tin sau.

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

tôi có chức năng này:

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

Hóa ra thực hiện điều này là khá chậm. Có cách nào để tăng tốc độ này không? Tôi đang nghĩ đến việc viết mã bằng tay với FindFirstFileW nhưng muốn tránh điều đó nếu có một cách được xây dựng theo cách nhanh hơn.

+0

Bạn đang tìm kiếm bao nhiêu tệp/thư mục? Độ sâu đệ quy là gì? –

+0

Nó khá nông, 371 dirs với trung bình 10 tập tin trong mỗi thư mục. một số dir có chứa các thư mục con khác –

+1

Có vẻ như P/Invoke là người chiến thắng ở đây. Nếu bạn vẫn cần thêm chuỗi công nhân tốc độ có thể giúp đỡ. –

Trả lời

36

Triển khai này cần được chỉnh sửa nhanh hơn 5-10 lần. phương pháp

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

mở rộng:

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 interop là:

[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

Dường như với tôi OP đang chạy quét qua nhiều tệp và thư mục. Vấn đề cơ bản là tra cứu thư mục 'đệ quy', chứ không phải chính mã được quản lý. Lý do là vì nó mang thời gian 5 phút xuống 30-60 giây, bởi vì không được quản lý, nó nhanh hơn. Nhưng lỗi thiết kế cơ bản vẫn là 'đệ quy'. Nếu bạn chuyển nó thành tra cứu thư mục 'lặp đi lặp lại', bạn có thể giảm xuống 7 giây trong mã được quản lý, nhanh hơn 4-8 lần so với (RecursiveScan2), mà không giới hạn bạn với Windows. –

+0

@Quandary Không phải lúc nào cũng đúng. Trong khi các phương pháp đệ quy thường chậm hơn vì chi phí đẩy và thông tin phương pháp popping đến và từ ngăn xếp cho mỗi cuộc gọi phương thức lặp lại, một cách tiếp cận lặp lại trong trường hợp này sẽ gần như đắt đỏ vì chi phí tăng thêm để duy trì hoạt động kiểm tra số lượng các hoạt động còn lại, ví dụ: http://stackoverflow.com/questions/26321366/fastest-way-to-get-directory-data-in-net – Alexandru

0

thử này (tức là làm việc khởi tạo đầu tiên, và sau đó tái sử dụng danh sách của bạn và các đối tượng DirectoryInfo của bạn):

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

Điều này làm cho không có sự khác biệt thực sự trong điểm chuẩn của tôi - phương pháp của bạn lấy 33156 mỏ mất 33498 ... một trong những interop mất 2872 mili giây .... hơn 10X nhanh hơn –

+0

đây là những gì tôi nghĩ đầu tiên, quá ... nhưng sau đó tôi đã thử nghiệm và nhận thấy nó có hiệu suất gần như giống nhau. =/ – Lucas

+0

thử qua chia sẻ smb –

5

Tùy thuộc vào bao nhiêu thời gian bạn đang cố gắng cạo tắt chức năng, nó có thể có giá trị trong khi bạn gọi hàm Win32 API trực tiếp, vì API hiện tại thực hiện nhiều xử lý bổ sung để kiểm tra những thứ bạn có thể không quan tâm.

Nếu bạn chưa làm như vậy và giả sử bạn không có ý định đóng góp cho dự án Mono, tôi khuyên bạn nên tải xuống Reflector và xem Microso như thế nào ft đã triển khai các cuộc gọi API bạn hiện đang sử dụng. Điều này sẽ cung cấp cho bạn một ý tưởng về những gì bạn cần phải gọi và những gì bạn có thể để lại. Ví dụ: bạn có thể chọn để tạo một trình lặp có tên là yield s thay vì một hàm trả về một danh sách, theo cách đó bạn không kết thúc việc lặp qua cùng một danh sách tên hai hoặc ba lần cho tất cả các cấp mã khác nhau.

+0

Mono đã làm gì với điều này ...? –

+2

@Cyril, tại http://mono-project.com/Contributing bạn có thể đọc về các yêu cầu của họ. Họ nói rõ ràng rằng "nếu bạn đã xem xét việc triển khai .NET của Microsoft hoặc mã nguồn được chia sẻ của họ, bạn sẽ không thể đóng góp cho Mono". Họ cũng đề cập đến phản xạ. – sisve

2

của nó khá nông, 371 dirs với trung bình 10 tệp trong mỗi thư mục. một số thư mục chứa các thư mục phụ khác

Đây chỉ là nhận xét, nhưng con số của bạn có vẻ khá cao. Tôi chạy dưới đây bằng cách sử dụng về cơ bản phương pháp đệ quy tương tự bạn đang sử dụng và thời gian của tôi là thấp hơn nhiều mặc dù tạo ra chuỗi đầu ra.

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

Tôi đã chạy mã trên trên một số thư mục khác nhau. Trên máy của tôi, cuộc gọi thứ hai để quét một cây thư mục thường nhanh hơn do bộ nhớ đệm theo thời gian chạy hoặc hệ thống tệp. Lưu ý rằng hệ thống này không phải là bất cứ điều gì quá đặc biệt, chỉ cần một máy trạm phát triển cũ 1yr.

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

Lo ngại rằng tôi không nhận được ngày tạo và sửa đổi, mã đã được sửa đổi để xuất kết quả này theo thời gian sau.

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

Lưu ý: System.Diagnostics.StopXem lớp được sử dụng để định thời gian.

+0

yerp tất cả các số của tôi là quét một phần mạng . do đó, dự kiến ​​của nó là khá cao –

+0

Vâng, tốc độ truy cập chậm hơn của bạn có ý nghĩa hơn bây giờ. –

2

Tôi chỉ chạy qua điều này. Triển khai tốt phiên bản gốc.

Phiên bản này, trong khi vẫn chậm hơn phiên bản sử dụng FindFirstFindNext, nhanh hơn một chút so với phiên bản .NET gốc của bạn.

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

Nó sẽ tạo ra cùng một kết quả như phiên bản gốc của bạn. Thử nghiệm của tôi cho thấy rằng phiên bản này mất khoảng 1,7 lần miễn là phiên bản sử dụng FindFirstFindNext. Thời gian thu được trong chế độ phát hành đang chạy mà không cần trình gỡ lỗi đính kèm.

Thật kỳ lạ, thay đổi GetFileSystemInfos thành EnumerateFileSystemInfos thêm khoảng 5% vào thời gian chạy trong các thử nghiệm của tôi. Tôi thay vì mong đợi nó chạy ở cùng một tốc độ hoặc có thể nhanh hơn bởi vì nó không phải tạo mảng các đối tượng FileSystemInfo.

Đoạn mã sau vẫn ngắn hơn, vì nó cho phép Khung làm việc chăm sóc đệ quy. Nhưng nó tốt hơn 15% đến 20% so với phiên bản trên.

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

Một lần nữa, nếu bạn thay đổi thành GetFileSystemInfos, nó sẽ hơi (nhưng chỉ hơi)) nhanh hơn.

Vì mục đích của tôi, giải pháp đầu tiên ở trên đủ nhanh. Phiên bản gốc chạy trong khoảng 1,6 giây. Phiên bản sử dụng DirectoryInfo chạy trong khoảng 2,9 giây. Tôi cho rằng nếu tôi chạy những lần quét này rất thường xuyên, tôi sẽ thay đổi ý định của mình.

5

Có lịch sử lâu dài của các phương pháp liệt kê tệp .NET chậm. Vấn đề là không có một cách tức thời liệt kê các cấu trúc thư mục lớn. Ngay cả câu trả lời được chấp nhận ở đây cũng có vấn đề với phân bổ GC.

Điều tốt nhất tôi có thể thực hiện được đưa vào thư viện của mình và được hiển thị là lớp FileFile (source) trong số CSharpTest.Net.IO namespace. Lớp này có thể liệt kê các tệp và thư mục mà không cần phân bổ GC không cần thiết và marshaling chuỗi.

Việc sử dụng rất đơn giản đủ, và tài sản RaiseOnAccessDenied sẽ bỏ qua các thư mục và tập tin người dùng không có quyền truy cập vào:

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

Đối với C địa phương của tôi: \ ổ đĩa này kết quả đầu ra như sau:

Liệt kê 810.046 tệp với tổng số 307.707.792,662 byte trong 232.876 giây.

Số dặm của bạn có thể thay đổi theo tốc độ ổ đĩa, nhưng đây là phương pháp nhanh nhất tôi đã tìm thấy liệt kê các tệp trong mã được quản lý.Tham số sự kiện là một lớp đột biến của loại FindFile.FileFoundEventArgs vì vậy hãy chắc chắn bạn không giữ một tham chiếu đến nó vì giá trị của nó sẽ thay đổi cho mỗi sự kiện được nêu ra.

Bạn cũng có thể lưu ý rằng phần tiếp xúc của DateTime chỉ ở dạng UTC. Lý do là việc chuyển đổi sang giờ địa phương là bán đắt. Bạn có thể cân nhắc sử dụng thời gian UTC để cải thiện hiệu suất thay vì chuyển đổi những giờ này thành giờ địa phương.

+0

Công cụ tuyệt vời! Nhưng tại sao bạn sử dụng 'Interlocked.Increment (ref total)' thay vì 'total ++' như bạn làm với 'size'. Tại sao tăng 'tổng số' trong một chuỗi an toàn và' kích thước' không? – mhu

+0

@mhu câu hỏi hay, 3 năm trước, tôi có thể đã nhớ nhưng hiện tại tôi đang thua lỗ hoàn toàn. Các cuộc gọi lại là đơn luồng và không có nhu cầu cho nó. –

+0

Đó là những gì tôi nghĩ. Cảm ơn bạn đã trả lời. – mhu

0

Gần đây tôi có cùng một câu hỏi, tôi nghĩ cũng tốt khi xuất tất cả thư mục và tệp vào tệp văn bản, sau đó sử dụng trình đọc luồng để đọc tệp văn bản, thực hiện những gì bạn muốn xử lý với nhiều chuỗi.

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

[cập nhật] Xin chào Moby, bạn là chính xác. Cách tiếp cận của tôi chậm hơn do chi phí đọc lại tệp văn bản đầu ra. Thực ra tôi đã dành chút thời gian để kiểm tra câu trả lời hàng đầu và cmd.exe với 2 triệu tệp.

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

Phương thức trả lời hàng đầu (53023) nhanh hơn cmd.exe (64907), chưa kể làm cách nào để cải thiện tệp văn bản đầu ra đọc. Mặc dù điểm ban đầu của tôi là cung cấp một câu trả lời không quá tệ, vẫn cảm thấy tiếc, ha.

+0

Điều đó sẽ chậm hơn vì nó đang ghi vào một tệp văn bản cùng một lúc, sau đó yêu cầu người gọi đọc lại tệp văn bản đó, sau đó xóa nó. Plus, chạy cmd.exe chính nó cho biết thêm chi phí. Nó sẽ không xử lý lỗi đúng cách, không thể đưa ra phản hồi khi nó đi cùng, ... –

Các vấn đề liên quan