Soru .NET'te yinelemeli bir dizinde taramanın daha hızlı bir yolu var mı?


.NET'te bir dizin tarayıcısı yazıyorum.

Her Dosya / Dir için aşağıdaki bilgilere ihtiyacım var.

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

Bu işleve sahibim:

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

Bu uygulamanın oldukça yavaş olduğunu ortaya koyuyor. Bunu hızlandırmak için herhangi bir yolu var mı? Bunu FindFirstFileW ile kodlamayı düşünüyorum ama daha hızlı inşa edilmiş bir yol varsa bundan kaçınmak istiyorum.


25
2018-04-07 04:33


Menşei


Kaç dosya / dizin aradınız? Yineleme derinliği nedir? - Robert Paulson
Her dizinde ortalama 10 dosya ile oldukça sığ, 371 dir. bazı dir'ler diğer alt dizinleri içerir - Sam Saffron
Burada kazanan P / Invoke gibi görünüyor. Hala daha hızlı çalışan iş parçacığına ihtiyacınız varsa yardımcı olabilir. - Robert Paulson
belki de orijinal ve güncelleştirilmiş zamanlamaları ve buna rastlayan diğer insanlara yardımcı olabilecek diğer istatistikleri belirtebilirsiniz. - Robert Paulson
Cevabı doğru olarak işaretlemeden önce yapacak - Sam Saffron


Cevaplar:


Biraz tweaking gerektiren bu uygulama, 5-10X daha hızlıdır.

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

uzatma yöntemi:

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

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

37
2018-04-07 05:00



Bana göre OP, birçok dosya ve klasör üzerinde bir tarama yapıyor. Temel sorun 'özyinelemeli' dizin geçişi, yönetilen kodun kendisi değil. Bunun sebebi, yönetilmeyen, daha hızlı olduğu için 5 dakikalık bir süreyi 30-60 saniyeye getirmesidir. Ama temel tasarım hatası hala 'özyinelemeli'. Bunu 'yinelemeli' dizin geçişine döndürürseniz, Windows'a sınırlamadan, bu kodun (RecursiveScan2) 4-8 kat daha hızlı olduğu yönetilen kodda 7 saniyeye kadar alabilirsiniz. - Stefan Steiger
@Quandary Her zaman doğru değil. Her yinelemeli yöntem çağrısı için istif ve veri yöntem bilgisinin yığılması ve çıkması pahasına genel olarak özyinelemeli yöntemler daha yavaş olmakla birlikte, bu durumda yinelemeli bir yaklaşım, bir koşuyu sürdürmek zorunda kalmanın ek maliyetinden dolayı neredeyse aynı derecede pahalı olacaktır. Kalan işlemlerin sayısının talibi, örneğin bkz: stackoverflow.com/questions/26321366/... - Alexandru


.NET dosya numaralandırma yöntemlerinin yavaş bir geçmişi vardır. Sorun, büyük dizin yapılarını sıralamak için anlık bir yol değil. Burada kabul edilen cevap bile, GC tahsisleri ile ilgili sorunlara sahiptir.

Yapabildiğim en iyi şey kütüphanemde tamamlandı ve FileFile (kaynak) sınıfında CSharpTest.Net.IO ad alanı. Bu sınıf, gereksiz GC tahsisleri ve dizi dizilimi olmadan dosya ve klasörleri sıralayabilir.

Kullanım yeterince basittir ve RaiseOnAccessDenied özelliği, kullanıcının erişemeyeceği dizinleri ve dosyaları atlar:

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

Yerel C: \ drive'ım için bu, aşağıdakileri çıkarır:

232.876 saniyede toplam 307,707,792,662 bayt olmak üzere 810.046 dosya numaralandırıldı.

Kilometreniz sürüş hızına göre değişebilir, ancak bu, yönetilen koddaki dosyaları numaralandırmanın en hızlı yöntemidir. Etkinlik parametresi, mutasyona uğrayan bir sınıftır. FindFile.FileFoundEventArgs Bu nedenle, her bir etkinlik için yükseltilen değerler değişeceğinden emin olmayın.

Ayrıca, DateTime'ın maruz kalmasının sadece UTC'de olduğunu unutmayın. Bunun nedeni, yerel zamana dönüşümün yarı pahalı olmasıdır. Bunları yerel saate dönüştürmek yerine performansı artırmak için UTC zamanlarını kullanmayı düşünebilirsiniz.


6
2017-09-17 18:01



Harika şeyler! Ama neden kullanıyorsun Interlocked.Increment(ref total) yerine total++ seninle olduğu gibi size. Neden artış total iş parçacığı güvenli bir şekilde ve size değil? - mhu
Mhu iyi soru, 3 yıl önce hatırlamış olabilirim ama şu an tam bir kayıptayım. Geri arama tek iş parçacığıdır ve buna gerek yoktur. - csharptest.net
Bende böyle düşünmüştüm. Cevabınız için teşekkür ederim. - mhu


İşleve ne kadar zaman harcayacağınıza bağlı olarak, mevcut API, ilgilenmeyebileceğiniz şeyleri kontrol etmek için fazladan bir çok işlem gerçekleştirdiğinden, doğrudan Win32 API işlevlerini çağırmak için zaman ayırmaya değer olabilir.

Henüz yapmadıysanız ve Mono projesine katkıda bulunmayı düşünmüyorsanız, indirmeyi kesinlikle tavsiye ederim. reflektör ve Microsoft'un şu anda kullanmakta olduğunuz API çağrılarını nasıl uyguladığı hakkında bir bakış. Bu size ne aramanız gerektiği ve neleri dışarıda bırakabileceğiniz hakkında bir fikir verecektir.

Örneğin, bir yineleyici oluşturmayı tercih edebilirsiniz. yields dizin isimleri, bir liste döndüren bir işlev yerine, bu sayede, tüm farklı kod düzeylerinde iki veya üç kez aynı ad listesinde yinelenmezsiniz.


5
2018-04-07 04:50



Mono'nun bununla ne alakası var? - Cyril Gupta
@Cyril, at mono-project.com/Contributing onların gereksinimleri hakkında okuyabilirsiniz. Açıkçası, "Microsoft'un .NET uygulamasına veya paylaşılan kaynak koduna bakmış olsaydınız, Mono'ya katkıda bulunamazsınız" ifadesini kullandılar. Ayrıca reflektörden bahsediyorlar. - sisve


Onun güzel sığ, 371 dirs ile   her dizinde ortalama 10 dosya.   bazı dir'ler diğer alt dizinleri içerir

Bu sadece bir yorum, ancak numaraların oldukça yüksek gibi görünüyor. Kullandığınız temelde aynı özyinelemeyi kullanarak aşağıda koştum ve zamanlarım dize çıktısına rağmen çok daha düşük.

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

Yukarıdaki kodu birkaç farklı dizinde çalıştırdım. Makinemde, bir dizin ağacını taramak için ikinci çağrı, çalışma zamanı veya dosya sistemi tarafından önbelleğe alınma nedeniyle genellikle daha hızlıydı. Bu sistemin çok özel bir şey olmadığını, sadece 1 yıllık bir geliştirme iş istasyonu olduğunu unutmayın.

// önbelleğe alınmış çağrı
Dirs = 150, dosyalar = 420, maksimum derinlik = 5
Alınan süre = 53 milisaniye

// önbelleğe alınmış çağrı
Dirs = 1117, dosyalar = 9076, maksimum derinlik = 11
Alınan süre = 433 milisaniye

// ilk arama
Dirs = 1052, dosyalar = 5903, maksimum derinlik = 12
Alınan süre = 11921 milisaniye

// ilk arama
Dirs = 793, dosyalar = 10748, maksimum derinlik = 10
Alınan süre = 5433 milisaniye (2. çalışma 363 milisaniye)

Oluşturulma ve değiştirilme tarihi almadığım için kod, aşağıdaki sürelerle de çıktı olarak değiştirildi.

// şimdi son güncelleme ve oluşturma zamanı kapılıyor.
Dirs = 150, dosyalar = 420, maksimum derinlik = 5
Alınan süre = 103 milisaniye (2. çalıştırma 93 milisaniye)

Dirs = 1117, dosyalar = 9076, maksimum derinlik = 11
Alınan süre = 992 milisaniye (2. çalıştırma 984 milisaniye)

Dirs = 793, dosyalar = 10748, maksimum derinlik = 10
Alınan süre = 1382 milisaniye (2. çalıştırma 735 milisaniye)

Dirs = 1052, dosyalar = 5903, maksimum derinlik = 12
Alınan süre = 936 milisaniye (2. çalışma 595 milisaniye)

Not: Zamanlama için kullanılan System.Diagnostics.StopWatch sınıfı.


2
2018-04-07 21:59



tüm sayılarım ağ paylaşımını taramaktır. bu yüzden oldukça yüksek olması bekleniyor - Sam Saffron
Evet, yavaş erişim hızlarınız şimdi daha mantıklı. - Robert Paulson


Sadece bunun üzerinden koştum. Yerel versiyonun iyi uygulanması.

Bu sürüm, hala kullanılan sürüme göre daha yavaş FindFirst ve FindNext, orijinal .NET sürümünüzden biraz daha hızlıdır.

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

Yerel sürümünüzle aynı çıktıyı üretmelidir. Testlerim, bu sürümün kullandığı sürümün yaklaşık 1,7 katı kadar sürdüğünü gösteriyor. FindFirst ve FindNext. Ayıklama modundan elde edilen zamanlamalar, hata ayıklayıcı eklenmeden çalışır.

Merakla, değiştirerek GetFileSystemInfos için EnumerateFileSystemInfos Testlerimde çalışma süresine yaklaşık% 5 ekler. Aynı hızda ya da muhtemelen daha hızlı koşmasını bekledim çünkü bu dizi dizisini oluşturmak zorunda değildi. FileSystemInfo nesneler.

Aşağıdaki kod daha kısadır, çünkü Çerçeve'nin özyinelemeye özen göstermesini sağlar. Ancak, yukarıdaki sürümden% 15 ila% 20 daha iyi.

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

Yine, bunu değiştirirseniz GetFileSystemInfos, biraz (ama sadece biraz) daha hızlı olacaktır.

Benim amaçlarım için, yukarıdaki ilk çözüm yeterince hızlıdır. Yerel sürüm yaklaşık 1,6 saniyede çalışır. Kullanan sürümü DirectoryInfo yaklaşık 2,9 saniyede çalışır. Bu taramaları çok sık çalıştırıyorsam, fikrimi değiştiririm.


2
2018-04-20 00:01





Kendimi bu çok iş parçacıklı kitaplıkta kullanırdım veya temel alırdım: http://www.codeproject.com/KB/files/FileFind.aspx


1
2018-04-07 05:49





Bunu deneyin (yani, önce ilklendirmeyi yapın ve sonra listenizi ve dizin nesnelerinizi yeniden kullanı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
2018-04-07 04:41



Bu benim kıyaslamamda gerçek bir fark yaratmıyor - yönteminiz 33156 benim 33498 almak ... ... 285 milisaniye sürüyor. - Sam Saffron
Ben de ilk düşündüğüm buydu ama sonra test ettim ve neredeyse aynı performansa sahip olduğunu fark ettim. = / - Lucas
bir smb paylaşımı denemek - Sam Saffron