Có gì đảm bảo về độ phức tạp trong thời gian chạy (Big-O) của các phương pháp LINQ?


120

Gần đây tôi đã bắt đầu sử dụng LINQ khá nhiều và tôi chưa thực sự thấy bất kỳ đề cập nào về độ phức tạp thời gian chạy đối với bất kỳ phương thức LINQ nào. Rõ ràng, có nhiều yếu tố đang diễn ra ở đây, vì vậy chúng ta hãy giới hạn cuộc thảo luận ở IEnumerablenhà cung cấp LINQ-to-Objects đơn giản . Hơn nữa, hãy giả sử rằng bất kỳ cái nào Funcđược chuyển vào dưới dạng bộ chọn / bộ đột biến / v.v. là một phép toán O (1) rẻ tiền.

Có vẻ như rõ ràng rằng tất cả các hoạt động đơn lẻ-pass ( Select, Where, Count, Take/Skip, Any/All, vv) sẽ là O (n), vì họ chỉ cần đi bộ trình tự một lần; mặc dù ngay cả điều này là tùy thuộc vào sự lười biếng.

Mọi thứ trở nên tồi tệ hơn đối với các hoạt động phức tạp hơn; tập giống như các nhà khai thác ( Union, Distinct, Except, vv) việc sử dụng GetHashCodetheo mặc định (afaik), vì vậy nó có vẻ hợp lý để cho rằng họ đang sử dụng một bảng băm trong nội bộ, làm cho các hoạt động này O (n) là tốt, nói chung. Điều gì về các phiên bản sử dụng một IEqualityComparer?

OrderBysẽ cần một sự sắp xếp, vì vậy rất có thể chúng ta đang xem xét O (n log n). Nếu nó đã được sắp xếp thì sao? Còn nếu tôi nói OrderBy().ThenBy()và cung cấp cùng một khóa cho cả hai?

Tôi có thể thấy GroupBy(và Join) bằng cách sử dụng sắp xếp hoặc băm. Đó là cái nào?

Containssẽ là O (n) trên a List, nhưng O (1) trên a HashSet- LINQ có kiểm tra vùng chứa bên dưới để xem liệu nó có thể tăng tốc mọi thứ không?

Và câu hỏi thực sự - cho đến nay, tôi đã tin tưởng rằng các hoạt động là hiệu quả. Tuy nhiên, tôi có thể gửi ngân hàng vào đó không? Ví dụ, các thùng chứa STL chỉ rõ mức độ phức tạp của mọi hoạt động. Có bất kỳ đảm bảo tương tự nào về hiệu suất LINQ trong đặc tả thư viện .NET không?

Câu hỏi khác (trả lời các bình luận): Tôi
chưa thực sự nghĩ về chi phí, nhưng tôi không mong đợi sẽ có rất nhiều thứ cho Linq-to-Objects đơn giản. Bài đăng CodingHorror đang nói về Linq-to-SQL, nơi tôi có thể hiểu việc phân tích cú pháp truy vấn và tạo SQL sẽ làm tăng thêm chi phí - có phải cũng có chi phí tương tự cho nhà cung cấp Đối tượng không? Nếu vậy, nó có gì khác nếu bạn đang sử dụng cú pháp khai báo hoặc hàm?


Mặc dù tôi thực sự không thể trả lời câu hỏi của bạn, tôi muốn nhận xét rằng nói chung, phần lớn của hiệu suất sẽ là "chi phí" so với chức năng cốt lõi. Tất nhiên điều này không xảy ra khi bạn có tập dữ liệu rất lớn (> 10k mục) nên tôi rất tò mò muốn biết trường hợp nào.
Henri

2
Re: "Có khác gì không nếu bạn đang sử dụng cú pháp khai báo hoặc hàm?" - trình biên dịch dịch cú pháp khai báo thành cú pháp chức năng để chúng giống nhau.
John Rasch

"Vùng chứa STL chỉ rõ mức độ phức tạp của mọi thao tác". Vùng chứa của .NET cũng chỉ rõ mức độ phức tạp của mọi thao tác. Các phần mở rộng Linq tương tự như các thuật toán STL, không phải các vùng chứa STL. Cũng giống như khi bạn áp dụng thuật toán STL cho vùng chứa STL, bạn cần kết hợp độ phức tạp của phần mở rộng Linq với độ phức tạp của (các) hoạt động vùng chứa .NET để phân tích đúng độ phức tạp của kết quả. Điều này bao gồm tính toán cho các chuyên môn hóa mẫu, như câu trả lời của Aaronaught đã đề cập.
Timbo

Một câu hỏi cơ bản là tại sao Microsoft không quan tâm nhiều hơn đến việc tối ưu hóa IList <T> sẽ có tiện ích hạn chế, vì nhà phát triển sẽ phải dựa vào hành vi không có giấy tờ nếu mã của họ phụ thuộc vào nó để hoạt động.
Edward Brey

AsParallel () trên Danh sách tập hợp kết quả; nên cung cấp cho bạn ~ O (1) <O (n)
Độ trễ

Câu trả lời:


121

Có rất, rất ít đảm bảo, nhưng có một số tối ưu hóa:

  • Phương pháp khuyến nông có sử dụng truy cập được lập chỉ mục, chẳng hạn như ElementAt, Skip, Lasthay LastOrDefault, sẽ kiểm tra xem có hay không các cụ loại cơ bản IList<T>, do đó bạn sẽ có được O (1) truy cập thay vì O (N).

  • Các Countkiểm tra phương pháp cho một ICollectionthực hiện, do đó hoạt động này là O (1) thay vì O (N).

  • Distinct, GroupBy Join, Và tôi cũng tin rằng các phương pháp thiết tập hợp ( Union, IntersectExcept) sử dụng băm, vì vậy họ cần được gần gũi với O (N) thay vì O (n ²).

  • Containskiểm tra việc ICollectiontriển khai, vì vậy nó có thể là O (1) nếu tập hợp cơ bản cũng là O (1), chẳng hạn như a HashSet<T>, nhưng điều này phụ thuộc vào cấu trúc dữ liệu thực tế và không được đảm bảo. Bộ băm ghi đè Containsphương thức, đó là lý do tại sao chúng là O (1).

  • OrderBy phương pháp sử dụng một nhanh ổn định, vì vậy chúng là trường hợp trung bình O (N log N).

Tôi nghĩ rằng điều đó bao gồm hầu hết nếu không phải là tất cả các phương pháp mở rộng được tích hợp sẵn. Thực sự có rất ít đảm bảo hiệu suất; Bản thân Linq sẽ cố gắng tận dụng các cấu trúc dữ liệu hiệu quả nhưng nó không phải là cách miễn phí để viết mã có khả năng không hiệu quả.


Làm thế nào về IEqualityComparerquá tải?
tzaman

@tzaman: Còn họ thì sao? Trừ khi bạn sử dụng một tùy chỉnh thực sự kém hiệu quả IEqualityComparer, tôi không thể lý do để nó ảnh hưởng đến độ phức tạp tiệm cận.
Aaronaught

1
Ô đúng rồi. Tôi đã không nhận ra các EqualityComparerdụng cụ GetHashCodecũng như Equals; nhưng tất nhiên điều đó có ý nghĩa hoàn hảo.
tzaman

2
@imgen: Các phép nối vòng lặp là O (N * M) tổng quát thành O (N²) cho các tập không liên quan. Linq sử dụng các phép nối băm là O (N + M), tổng quát thành O (N). Điều đó giả định là một hàm băm khá ổn, nhưng điều đó khó có thể gây nhầm lẫn trong .NET.
Aaronaught

1
Orderby().ThenBy()vẫn N logNhay là nó (N logN) ^2hay cái gì như thế?
M.kazem Akhgary

10

Từ lâu, tôi đã biết rằng .Count()trả về .Countnếu phép liệt kê là một IList.

Nhưng tôi đã luôn luôn là một chút mệt mỏi về sự phức tạp thời gian chạy của các hoạt động Set: .Intersect(), .Except(), .Union().

Đây là triển khai BCL (.NET 4.0 / 4.5) được dịch ngược cho .Intersect()(nhận xét của tôi):

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

Kết luận:

  • hiệu suất là O (M + N)
  • việc triển khai không tận dụng khi các bộ sưu tập đã được đặt sẵn. (Nó có thể không nhất thiết phải đơn giản, vì được sử dụng IEqualityComparer<T>cũng cần phải phù hợp.)

Để có sự hoàn chỉnh, đây là cách triển khai cho .Union().Except().

Cảnh báo spoiler: chúng cũng có độ phức tạp 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

Tất cả những gì bạn thực sự có thể tin tưởng là các phương pháp Enumerable được viết tốt cho trường hợp chung và sẽ không sử dụng các thuật toán ngây thơ. Có thể có nội dung của bên thứ ba (blog, v.v.) mô tả các thuật toán thực sự đang được sử dụng, nhưng chúng không chính thức hoặc không được đảm bảo theo nghĩa là thuật toán STL.

Để minh họa, đây là mã nguồn được phản ánh (do ILSpy cung cấp) cho Enumerable.Counttừ 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;
    }
}

Như bạn có thể thấy, cần phải cố gắng tránh giải pháp ngây thơ là chỉ cần liệt kê mọi phần tử.


lặp thông qua toàn bộ đối tượng để có được những Count () nếu nó là một IEnnumerable có vẻ khá ngây thơ với tôi ...
Zonko

4
@Zonko: Tôi không hiểu quan điểm của bạn. Tôi đã sửa đổi câu trả lời của mình để cho thấy điều Enumerable.Countđó không lặp lại trừ khi không có giải pháp thay thế rõ ràng nào. Làm thế nào bạn sẽ làm cho nó bớt ngây thơ hơn?
Marcelo Cantos

Vâng, vâng, các phương pháp được thực hiện theo cách hiệu quả nhất với nguồn. Tuy nhiên, cách hiệu quả nhất đôi khi là một thuật toán ngây thơ, và người ta nên cẩn thận khi sử dụng linq vì nó ẩn độ phức tạp thực sự của các cuộc gọi. Nếu bạn không quen với cấu trúc cơ bản của các đối tượng mà bạn đang thao tác, bạn có thể dễ dàng sử dụng các phương pháp sai cho nhu cầu của mình.
Zonko

@MarceloCantos Tại sao mảng không được xử lý? Nó cũng tương tự đối với phương thức ElementAtOrDefault tham chiếu
nguồn.microsoft.com/#System.Core/System/Linq/

@Freshblood Họ là. (Mảng triển khai ICollection.) Tuy nhiên, bạn không biết về ElementAtOrDefault. Tôi đoán các mảng cũng triển khai ICollection <T>, nhưng .Net của tôi khá lâu ngày.
Marcelo Cantos

3

Tôi vừa phá vỡ tấm phản xạ và họ kiểm tra loại cơ bản khi Containsđược gọi.

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

Câu trả lời chính xác là "nó phụ thuộc". nó phụ thuộc vào loại IEnumerable cơ bản là gì. tôi biết rằng đối với một số bộ sưu tập (như bộ sưu tập triển khai ICollection hoặc IList) có các đường dẫn đặc biệt được sử dụng, Tuy nhiên, việc triển khai thực tế không được đảm bảo thực hiện bất kỳ điều gì đặc biệt. ví dụ, tôi biết rằng ElementAt () có một trường hợp đặc biệt cho các bộ sưu tập có thể lập chỉ mục, tương tự với Count (). Nhưng nói chung, bạn có thể nên giả định hiệu suất O (n) trong trường hợp xấu nhất.

Nói chung, tôi không nghĩ rằng bạn sẽ tìm thấy loại đảm bảo hiệu suất mà bạn muốn, mặc dù nếu bạn gặp phải một vấn đề hiệu suất cụ thể với toán tử linq, bạn luôn có thể thực hiện lại nó cho bộ sưu tập cụ thể của mình. Ngoài ra, có nhiều blog và dự án mở rộng mở rộng Linq thành Đối tượng để thêm các loại đảm bảo hiệu suất này. kiểm tra LINQ được lập chỉ mục mở rộng và thêm vào bộ điều hành để có thêm lợi ích về hiệu suất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.