มีการรับประกันอะไรบ้างเกี่ยวกับความซับซ้อนของเวลาทำงาน (Big-O) ของวิธีการ LINQ?


120

ฉันเพิ่งเริ่มใช้ LINQ ได้ไม่นานและฉันไม่เห็นการกล่าวถึงความซับซ้อนของรันไทม์สำหรับวิธีการใด ๆ ของ LINQ เลย เห็นได้ชัดว่ามีหลายปัจจัยในการเล่นที่นี่ดังนั้นขอ จำกัด การสนทนาเฉพาะIEnumerableผู้ให้บริการ LINQ-to-Objects ธรรมดา นอกจากนี้สมมติว่าสิ่งที่Funcส่งผ่านมาเป็นตัวเลือก / ตัวเปลี่ยน / ฯลฯ เป็นการดำเนินการ O (1) ราคาถูก

ดูเหมือนว่าเห็นได้ชัดว่าทุกการดำเนินงานที่ผ่านเดียว ( Select, Where, Count, Take/Skip, Any/Allฯลฯ ) จะ O (n) เนื่องจากพวกเขาจะต้องเดินตามลำดับเมื่อ; แม้ว่าเรื่องนี้จะเป็นเรื่องขี้เกียจก็ตาม

สิ่งต่าง ๆ มีความซับซ้อนมากขึ้นสำหรับการดำเนินการที่ซับซ้อนมากขึ้น ชุดเหมือนผู้ประกอบการ ( Union, Distinct, Exceptฯลฯ ) การทำงานโดยใช้GetHashCodeค่าเริ่มต้น (AFAIK) ดังนั้นจึงดูเหมือนว่าเหมาะสมที่จะถือว่าพวกเขากำลังใช้กัญชาตารางภายในทำให้การดำเนินงานเหล่า O (n) เช่นกันโดยทั่วไป สิ่งที่เกี่ยวกับเวอร์ชันที่ใช้IEqualityComparer?

OrderByจะต้องมีการเรียงลำดับดังนั้นส่วนใหญ่เราจะดูที่ O (n log n) จะเป็นอย่างไรหากเรียงลำดับแล้ว แล้วถ้าฉันพูดOrderBy().ThenBy()และให้คีย์เดียวกันกับทั้งสองอย่างล่ะ?

ฉันเห็นGroupBy(และJoin) โดยใช้การเรียงลำดับหรือการแฮช มันคืออะไร?

Containsจะเป็น O (n) บน a Listแต่ O (1) บน a HashSet- LINQ ตรวจสอบคอนเทนเนอร์ที่อยู่เบื้องหลังเพื่อดูว่าสามารถเร่งความเร็วได้หรือไม่?

และคำถามที่แท้จริง - จนถึงตอนนี้ฉันเชื่อมั่นว่าการดำเนินงานมีประสิทธิภาพ อย่างไรก็ตามฉันสามารถฝากเงินได้หรือไม่? ตัวอย่างเช่นคอนเทนเนอร์ STL ระบุความซับซ้อนของทุกการดำเนินการอย่างชัดเจน มีการรับประกันที่คล้ายกันเกี่ยวกับประสิทธิภาพของ LINQ ในข้อกำหนดไลบรารี. NET หรือไม่

คำถามเพิ่มเติม (ในการตอบกลับความคิดเห็น):
ไม่ได้คิดเรื่องค่าโสหุ้ย แต่ฉันไม่ได้คาดหวังว่าจะมี Linq-to-Objects ธรรมดา ๆ โพสต์ CodingHorror กำลังพูดถึง Linq-to-SQL ซึ่งฉันสามารถเข้าใจการแยกวิเคราะห์แบบสอบถามและการสร้าง SQL จะเพิ่มต้นทุน - มีค่าใช้จ่ายที่ใกล้เคียงกันสำหรับผู้ให้บริการ Objects ด้วยหรือไม่ ถ้าเป็นเช่นนั้นจะแตกต่างกันหรือไม่หากคุณใช้ไวยากรณ์ที่เปิดเผยหรือใช้งานได้


แม้ว่าฉันจะไม่สามารถตอบคำถามของคุณได้ แต่ฉันต้องการแสดงความคิดเห็นว่าโดยทั่วไปส่วนใหญ่ของประสิทธิภาพจะเป็น "ค่าใช้จ่าย" เมื่อเทียบกับฟังก์ชันหลัก แน่นอนว่านี่ไม่ใช่กรณีที่คุณมีชุดข้อมูลขนาดใหญ่มาก (> 10k รายการ) ดังนั้นฉันจึงอยากรู้ว่าในกรณีนี้คุณต้องการทราบ
Henri

2
Re: "มันแตกต่างกันไหมถ้าคุณใช้ไวยากรณ์ที่เปิดเผยหรือใช้งานได้" - คอมไพเลอร์จะแปลไวยากรณ์ที่เปิดเผยเป็นไวยากรณ์เชิงฟังก์ชันดังนั้นจึงเหมือนกัน
John Rasch

"คอนเทนเนอร์ STL ระบุความซับซ้อนของทุกการดำเนินการอย่างชัดเจน" คอนเทนเนอร์. NET ยังระบุความซับซ้อนของทุกการดำเนินการอย่างชัดเจน ส่วนขยาย Linq คล้ายกับอัลกอริทึม STL ไม่ใช่คอนเทนเนอร์ STL เช่นเดียวกับเมื่อคุณใช้อัลกอริทึม STL กับคอนเทนเนอร์ STL คุณจะต้องรวมความซับซ้อนของส่วนขยาย Linq กับความซับซ้อนของการดำเนินการคอนเทนเนอร์. NET เพื่อวิเคราะห์ความซับซ้อนของผลลัพธ์อย่างถูกต้อง ซึ่งรวมถึงการบัญชีสำหรับความเชี่ยวชาญพิเศษของเทมเพลตดังที่คำตอบของ Aaronaught กล่าวถึง
Timbo

คำถามพื้นฐานคือเหตุใด Microsoft จึงไม่กังวลมากขึ้นว่าการเพิ่มประสิทธิภาพ IList <T> จะเป็นประโยชน์อย่าง จำกัด เนื่องจากนักพัฒนาจะต้องพึ่งพาพฤติกรรมที่ไม่มีเอกสารหากรหัสของเขาขึ้นอยู่กับว่าจะมีประสิทธิภาพ
Edward Brey

AsParallel () ในรายการชุดผลลัพธ์; ควรให้คุณ ~ O (1) <O (n)
เวลาแฝง

คำตอบ:


121

มีการรับประกันน้อยมาก แต่มีการเพิ่มประสิทธิภาพบางประการ:

  • วิธีการขยายที่ใช้จัดทำดัชนีการเข้าถึงเช่นElementAt, Skip, LastหรือLastOrDefaultจะตรวจสอบเพื่อดูหรือไม่ว่าการดำเนินการประเภทพื้นฐานIList<T>เพื่อให้คุณได้รับ O (1) การเข้าถึงแทน O (N)

  • CountวิธีการตรวจสอบการICollectionดำเนินงานเพื่อให้การดำเนินการนี้เป็น O (1) แทน O (N)

  • Distinct, GroupBy Joinและผมเชื่อว่ายังมีวิธีการติดตั้งรวม ( Union, IntersectและExcept) ใช้คร่ำเครียดดังนั้นพวกเขาควรจะใกล้เคียงกับ O (n) แทน O (N²)

  • ContainsตรวจสอบICollectionการนำไปใช้งานดังนั้นจึงอาจเป็น O (1) หากคอลเลกชันพื้นฐานยังเป็น O (1) เช่น a HashSet<T>แต่ขึ้นอยู่กับโครงสร้างข้อมูลจริงและไม่รับประกัน ชุดแฮชจะแทนที่Containsเมธอดนั่นคือเหตุผลว่าทำไมจึงเป็น O (1)

  • OrderBy วิธีการใช้ Quicksort ที่เสถียรดังนั้นจึงเป็นกรณีเฉลี่ย O (N log N)

ฉันคิดว่าครอบคลุมมากที่สุดหากไม่ใช่วิธีการขยายในตัวทั้งหมด มีการรับประกันประสิทธิภาพน้อยมาก Linq เองจะพยายามใช้ประโยชน์จากโครงสร้างข้อมูลที่มีประสิทธิภาพ แต่ไม่ใช่การเขียนโค้ดที่อาจไม่มีประสิทธิภาพ


แล้วIEqualityComparerโอเวอร์โหลดล่ะ?
tzaman

@tzaman: แล้วพวกเขาล่ะ? เว้นแต่คุณจะใช้ประเพณีที่ไม่มีประสิทธิภาพจริงๆIEqualityComparerฉันไม่สามารถหาเหตุผลว่ามันจะส่งผลต่อความซับซ้อนของ asymptotic ได้
Aaronaught

1
โอ้ใช่. ฉันไม่ได้ตระหนักถึงEqualityComparerการดำเนินการGetHashCodeเช่นเดียวกับEquals; แต่แน่นอนว่ามันสมเหตุสมผล
tzaman

2
@imgen: การรวมแบบวนซ้ำคือ O (N * M) ซึ่งรวมเป็น O (N²) สำหรับชุดที่ไม่เกี่ยวข้อง Linq ใช้การรวมแฮชซึ่งเป็น O (N + M) ซึ่งเป็นลักษณะทั่วไปของ O (N) ซึ่งถือว่าเป็นฟังก์ชันแฮชที่ดีเพียงครึ่งเดียว แต่ก็ยากที่จะทำผิดพลาดใน. NET
Aaronaught

1
เป็นOrderby().ThenBy()ยังN logNหรือจะเป็น(N logN) ^2หรือสิ่งที่ต้องการนั้น
M.kazem Akhgary

10

ฉันรู้มานานแล้วว่า.Count()จะส่งคืน.Countหากการแจงนับเป็นIListไฟล์.

แต่ผมก็เสมอ bit เบื่อเกี่ยวกับความซับซ้อนของเวลาทำงานของการดำเนินงานชุด: .Intersect(), ,.Except().Union()

นี่คือการใช้งาน BCL (.NET 4.0 / 4.5) แบบถอดรหัสสำหรับ.Intersect()(ความคิดเห็นของฉัน):

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

สรุป:

  • ประสิทธิภาพคือ O (M + N)
  • การใช้งานไม่ได้ใช้ประโยชน์เมื่อมีการตั้งค่าคอลเล็กชันไว้แล้ว (อาจไม่จำเป็นต้องตรงไปตรงมาเพราะคำที่ใช้IEqualityComparer<T>นั้นต้องตรงกันด้วย)

เพื่อความสมบูรณ์ที่นี่มีการใช้งานสำหรับการและ.Union().Except()

การแจ้งเตือนสปอยเลอร์: พวกเขาก็มี ความซับซ้อนO (N + M)เช่นกัน

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

สิ่งที่คุณสามารถทำได้ก็คือเมธอด Enumerable นั้นเขียนไว้อย่างดีสำหรับกรณีทั่วไปและจะไม่ใช้อัลกอริทึมที่ไร้เดียงสา อาจมีเนื้อหาของบุคคลที่สาม (บล็อก ฯลฯ ) ที่อธิบายถึงอัลกอริทึมที่ใช้งานจริง แต่สิ่งเหล่านี้ไม่เป็นทางการหรือรับประกันในแง่ที่อัลกอริทึม STL เป็น

เพื่อเป็นตัวอย่างนี่คือซอร์สโค้ดที่สะท้อนให้เห็น (ได้รับความอนุเคราะห์จาก ILSpy) Enumerable.Countจาก System.Core:

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

อย่างที่คุณเห็นต้องใช้ความพยายามบางอย่างเพื่อหลีกเลี่ยงวิธีแก้ปัญหาที่ไร้เดียงสาในการแจกแจงทุกองค์ประกอบ


การวนซ้ำวัตถุทั้งหมดเพื่อรับ Count () ถ้าเป็น IE ที่ไม่สามารถนับได้ดูเหมือนจะไร้เดียงสาสำหรับฉัน ...
Zonko

4
@ โซนโกะ: ฉันไม่เข้าใจประเด็นของคุณ ฉันได้แก้ไขคำตอบของฉันเพื่อแสดงว่าEnumerable.Countมันไม่ได้ทำซ้ำเว้นแต่จะไม่มีทางเลือกอื่นที่ชัดเจน คุณจะทำให้ไร้เดียงสาน้อยลงได้อย่างไร?
Marcelo Cantos

ใช่แล้ววิธีการนี้ถูกนำไปใช้อย่างมีประสิทธิภาพสูงสุดตามแหล่งที่มา อย่างไรก็ตามวิธีที่มีประสิทธิภาพที่สุดในบางครั้งอาจเป็นอัลกอริทึมที่ไร้เดียงสาและควรระมัดระวังเมื่อใช้ linq เพราะซ่อนความซับซ้อนของการโทรไว้ หากคุณไม่คุ้นเคยกับโครงสร้างพื้นฐานของวัตถุที่คุณกำลังจัดการคุณอาจใช้วิธีการที่ไม่ถูกต้องตามความต้องการของคุณได้อย่างง่ายดาย
Zonko

@MarceloCantos ทำไมอาร์เรย์ไม่ได้รับการจัดการ มันเหมือนกันสำหรับเมธอด ElementAtOrDefault referencesource.microsoft.com/#System.Core/System/Linq/…
Freshblood

@Freshblood พวกเขาคือ. (อาร์เรย์ใช้ ICollection) ไม่ทราบเกี่ยวกับ ElementAtOrDefault ฉันเดาว่าอาร์เรย์ใช้ ICollection <T> ด้วย แต่. Net ของฉันวันนี้ค่อนข้างเป็นสนิม
Marcelo Cantos

3

ฉันเพิ่งแตกตัวสะท้อนแสงและพวกเขาตรวจสอบประเภทพื้นฐานเมื่อContainsถูกเรียก

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

คำตอบที่ถูกต้องคือ "ขึ้นอยู่กับ" ขึ้นอยู่กับประเภทของ IEnumerable ที่เป็นพื้นฐาน ฉันรู้ว่าสำหรับบางคอลเลกชัน (เช่นคอลเลกชันที่ใช้ ICollection หรือ IList) จะมี codepath พิเศษที่ใช้อย่างไรก็ตามการใช้งานจริงไม่รับประกันว่าจะทำอะไรเป็นพิเศษ ตัวอย่างเช่นฉันรู้ว่า ElementAt () มีกรณีพิเศษสำหรับคอลเล็กชันที่จัดทำดัชนีได้เช่นเดียวกับ Count () แต่โดยทั่วไปแล้วคุณควรถือว่าประสิทธิภาพ O (n) ในกรณีที่แย่ที่สุด

โดยทั่วไปฉันไม่คิดว่าคุณจะพบประเภทของการรับประกันประสิทธิภาพที่คุณต้องการแม้ว่าคุณจะประสบปัญหาด้านประสิทธิภาพโดยเฉพาะกับตัวดำเนินการ linq คุณสามารถนำมาใช้ใหม่สำหรับคอลเลกชันเฉพาะของคุณได้ตลอดเวลา นอกจากนี้ยังมีบล็อกและโครงการความสามารถในการขยายจำนวนมากที่ขยาย Linq ไปยัง Objects เพื่อเพิ่มการรับประกันประสิทธิภาพประเภทนี้ ตรวจสอบดัชนี LINQ ซึ่งขยายและเพิ่มไปยังชุดตัวดำเนินการเพื่อประโยชน์ด้านประสิทธิภาพที่มากขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.