LINQ yöntemlerinin çalışma zamanı karmaşıklığı (Big-O) konusunda hangi garantiler var?


120

Kısa bir süre önce LINQ kullanmaya başladım ve LINQ yöntemlerinden herhangi biri için çalışma zamanı karmaşıklığından gerçekten bahsetmedim. Açıkçası, burada rol oynayan birçok faktör var, bu yüzden tartışmayı basit IEnumerableLINQ-to-Objects sağlayıcısıyla sınırlayalım . Ayrıca, Funcseçici / mutatör / vb. Olarak geçirilenlerin ucuz bir O (1) işlemi olduğunu varsayalım .

Her şey tek geçişli işlemler (yani açık görünüyor Select, Where, Count, Take/Skip, Any/Allbunlar sadece bir kez diziyi yürümek gerekiyor çünkü, vs.), O (n) olacaktır; Bu bile tembellik olsa da.

Daha karmaşık işlemler için işler daha belirsizdir; set gibi operatörler ( Union, Distinct, Exceptvb) kullanılarak iş GetHashCodevarsayılan olarak (afaik), bunların genel olarak sıra bu işlemler O (n) yapım içten bir karma-tablo kullanıyorsanız varsaymak makul görünmektedir böylece. Peki ya bir kullanan sürümler IEqualityComparer?

OrderBybir sıralamaya ihtiyaç duyar, bu yüzden büyük olasılıkla O (n log n) 'ye bakıyoruz. Ya zaten sıralanmışsa? OrderBy().ThenBy()Her ikisine de aynı anahtarı söyleyip sağlasam nasıl olur ?

Sıralama veya karma kullanarak GroupBy(ve Join) görebiliyordum . Hangisi?

Containsa üzerinde O (n) List, ancak a üzerinde O (1) olur HashSet- LINQ, işleri hızlandırıp hızlandıramayacağını görmek için temeldeki kapsayıcıyı kontrol ediyor mu?

Ve asıl soru - şimdiye kadar, operasyonların başarılı olduğuna inanıyorum. Ancak, buna güvenebilir miyim? Örneğin STL konteynerleri, her işlemin karmaşıklığını açıkça belirtir. .NET kitaplığı belirtiminde LINQ performansına ilişkin benzer garantiler var mı?

Daha fazla soru (yorumlara yanıt olarak):
Genel giderler hakkında gerçekten düşünmemiştim, ancak basit Linq-to-Objects için çok fazla şey olmasını beklemiyordum. CodingHorror yazısı, sorguyu ayrıştırmanın ve SQL yapmanın maliyet katacağını anlayabildiğim Linq-to-SQL'den bahsediyor - Objects sağlayıcısı için de benzer bir maliyet var mı? Öyleyse, bildirime dayalı veya işlevsel sözdizimini kullanıyorsanız farklı mıdır?


Sorunuzu gerçekten yanıtlayamasam da, genel olarak performansın büyük kısmının temel işlevlere kıyasla "ek yük" olacağını söylemek istiyorum. Elbette, çok büyük veri kümeleriniz (> 10.000 öğe) olduğunda durum böyle değildir, bu yüzden merak ediyorum, hangi durumda bilmek istiyorsunuz.
Henri

2
Re: "bildirim temelli veya işlevsel sözdizimi kullanıyorsanız farklı mıdır?" - derleyici bildirim temelli sözdizimini işlevsel sözdizimine çevirir, böylece aynı olurlar.
John Rasch

"STL kapsayıcıları her işlemin karmaşıklığını açıkça belirtir". NET kapsayıcıları ayrıca her işlemin karmaşıklığını da açıkça belirtir. Linq uzantıları, STL kapsayıcılarına değil, STL algoritmalarına benzer. Bir STL kapsayıcısına bir STL algoritması uyguladığınızda olduğu gibi, ortaya çıkan karmaşıklığı doğru şekilde analiz etmek için Linq uzantısının karmaşıklığını .NET kapsayıcı işlemlerinin karmaşıklığıyla birleştirmeniz gerekir. Bu, Aaronaught'ın cevabının belirttiği gibi, şablon uzmanlıklarının muhasebesini içerir.
Timbo

Temel bir soru, Microsoft'un neden bir IList <T> optimizasyonunun sınırlı fayda sağlayacağı konusunda daha fazla endişelenmediği, çünkü bir geliştiricinin kodunun performansa bağlı olması durumunda belgelenmemiş davranışa güvenmek zorunda kalacağı düşünülüyor.
Edward Brey

Ortaya çıkan küme Listesinde AsParallel (); vermeli ~ O (1) <O (n)
Gecikme

Yanıtlar:


121

Çok, çok az garanti vardır, ancak birkaç optimizasyon vardır:

  • Gibi endeksli erişim kullanmak Uzatma yöntemleri, ElementAt, Skip, Lastveya LastOrDefault, olsun veya olmasın yatan tipi uygular kontrol eder IList<T>, böylece O (N) O (1) erişim yerine almak.

  • CountBir yöntemi kontrol ICollectionuygulanması, bu nedenle bu işlem olduğu O (1) yerine, O (N).

  • Distinct, GroupBy Join, Ve set-agregasyon yöntemleri de inanıyoruz ( Union, Intersectve Exceptbunlar, O (K) yerine O (N²) yakın olmalıdır, böylece kullanım karma).

  • ContainsBir kontrol eder ICollectionuygulama, o kadar olabilir , altta yatan toplama gibi a da O (1) ise, O (1) olması HashSet<T>, ama bu gerçek veri yapısına bağlıdır ve garanti edilmez. Karma kümeler Containsyöntemi geçersiz kılar , bu yüzden O (1) 'dir.

  • OrderBy yöntemler kararlı bir hızlı sıralama kullanır, bu nedenle bunlar O (N log N) ortalama durumlardır.

Sanırım bu, yerleşik uzatma yöntemlerinin tamamını olmasa da çoğunu kapsıyor. Gerçekten çok az performans garantisi vardır; Linq'in kendisi verimli veri yapılarından yararlanmaya çalışacaktır, ancak potansiyel olarak verimsiz kod yazmak için ücretsiz bir geçiş değildir.


IEqualityComparerAşırı yüklemelere ne dersiniz ?
tzaman

@tzaman: Ya onlar? Gerçekten verimsiz bir özel kullanmadığınız sürece IEqualityComparer, asimptotik karmaşıklığı etkilemesine neden olamam.
Aaronaught

1
Oh, doğru. Fark etmemiştim EqualityCompareruygular GetHashCodesıra sıra Equals; ama elbette bu çok mantıklı.
tzaman

2
@imgen: Döngü birleşimleri, ilgisiz kümeler için O (N²) 'ye genelleyen O (N * M)' dir. Linq, O (N) 'ye genelleyen O (N + M) olan hash birleşimlerini kullanır. Bu, yarı yarıya iyi bir hash işlevi varsayar, ancak bunu .NET'te karıştırmak zordur.
Aaronaught

1
olduğu Orderby().ThenBy()hala N logNya öyle (N logN) ^2veya böyle bir şey?
M.kazem Akhgary

10

O kadar uzun tanıdığım .Count()döner .Countnumaralandırma bir ise IList.

Ama Seti operasyonlarının çalışma zamanı karmaşıklığı hakkında yorgun biraz hep: .Intersect(), .Except(), .Union().

İşte .Intersect()(benim yorumlar ) için derlenmiş BCL (.NET 4.0 / 4.5) uygulaması :

private static IEnumerable<TSource> IntersectIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)                    // O(M)
    set.Add(source);                                    // O(1)

  foreach (TSource source in first)                     // O(N)
  {
    if (set.Remove(source))                             // O(1)
      yield return source;
  }
}

Sonuç:

  • performans O (M + N)
  • koleksiyonlar zaten kümeler olduğunda uygulama avantaj sağlamaz . (Kullanılanın da eşleşmesi gerektiğinden, ille de basit olmayabilir .)IEqualityComparer<T>

Bütünlüğü sağlamak için, burada yönelik uygulamalar vardır .Union()ve .Except().

Spoiler uyarısı: onlar da O (N + M) karmaşıklığına sahiptir.

private static IEnumerable<TSource> UnionIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
  foreach (TSource source in second)
  {
    if (set.Add(source))
      yield return source;
  }
}


private static IEnumerable<TSource> ExceptIterator<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer)
{
  Set<TSource> set = new Set<TSource>(comparer);
  foreach (TSource source in second)
    set.Add(source);
  foreach (TSource source in first)
  {
    if (set.Add(source))
      yield return source;
  }
}

8

Gerçekten güvenebileceğiniz tek şey, Numaralandırılabilir yöntemlerin genel durum için iyi yazılmış olması ve saf algoritmalar kullanmamasıdır. Muhtemelen kullanımda olan algoritmaları tanımlayan üçüncü taraf şeyler (bloglar, vb.) Vardır, ancak bunlar STL algoritmalarının olduğu anlamda resmi veya garanti değildir.

Örnek olarak, Enumerable.CountSystem.Core'dan yansıtılan kaynak kodu (ILSpy'ın izniyle) :

// System.Linq.Enumerable
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    checked
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        ICollection<TSource> collection = source as ICollection<TSource>;
        if (collection != null)
        {
            return collection.Count;
        }
        ICollection collection2 = source as ICollection;
        if (collection2 != null)
        {
            return collection2.Count;
        }
        int num = 0;
        using (IEnumerator<TSource> enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                num++;
            }
        }
        return num;
    }
}

Gördüğünüz gibi, her öğeyi basitçe sıralamanın naif bir çözümünden kaçınmak biraz çaba gerektiriyor.


o ... Bir IEnnumerable bana oldukça naif görünüyor eğer bütün nesnesi aracılığıyla yineleme) (Kont almak
Zonko

4
@Zonko: Ne demek istediğini anlamıyorum. Cevabımı, Enumerable.Countbariz bir alternatif olmadığı sürece yinelemeyeceğini gösterecek şekilde değiştirdim. Nasıl daha az saf hale getirirdin?
Marcelo Cantos

Evet, yöntemler kaynağa göre en verimli şekilde uygulanıyor. Bununla birlikte, en verimli yol bazen saf bir algoritmadır ve linq kullanırken dikkatli olunmalıdır çünkü aramaların gerçek karmaşıklığını gizler. Manipüle ettiğiniz nesnelerin temel yapısına aşina değilseniz, ihtiyaçlarınız için yanlış yöntemleri kolayca kullanabilirsiniz.
Zonko

@MarceloCantos Neden diziler işlenmiyor? ElementAtOrDefault yöntem referansları
Freshblood

@Freshblood Onlar. (Diziler ICollection uygular.) Yine de ElementAtOrDefault hakkında bir şey bilmiyorum. Tahminimce diziler de ICollection <T> uyguluyor, ancak benim .Net'im bugünlerde oldukça paslı.
Marcelo Cantos

3

Reflektörü kırdım ve Containsçağrıldığında temeldeki türü kontrol ediyorlar .

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
{
    ICollection<TSource> is2 = source as ICollection<TSource>;
    if (is2 != null)
    {
        return is2.Contains(value);
    }
    return source.Contains<TSource>(value, null);
}

3

Doğru cevap "duruma göre değişir" dir. temeldeki IEnumerable'ın ne tür olduğuna bağlıdır. Bazı koleksiyonlar için (ICollection veya IList'i uygulayan koleksiyonlar gibi) kullanılan özel kod yolları olduğunu biliyorum, Ancak gerçek uygulamanın özel bir şey yapması garanti edilmez. örneğin, Count () ile benzer şekilde, ElementAt () 'ın indekslenebilir koleksiyonlar için özel bir durumu olduğunu biliyorum. Ancak genel olarak, muhtemelen en kötü durumdaki O (n) performansını varsaymalısınız.

Genel olarak, istediğiniz performans garantilerini bulacağınızı sanmıyorum, ancak bir linq operatörü ile belirli bir performans sorunuyla karşılaşırsanız, bunu her zaman kendi koleksiyonunuz için yeniden uygulayabilirsiniz. Ayrıca, bu tür performans garantilerini eklemek için Linq'i Nesnelere genişleten birçok blog ve genişletilebilirlik projesi vardır. kontrol Endeksli LINQ uzanır ve daha fazla performans yararları için operatör kümesine ekler.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.