2012-04-25 10 views
7

Giả sử tôi có đoạn mã sau:Giá trị tính toán LINQ cache có được tính không?

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
"); 
Enumerable.Range (1, 100) 
    .Select (s => X.Elements() 
     .Select (t => Int32.Parse (t.Attribute ("v").Value)) 
     .Aggregate (s, (t, u) => t * u) 
    ) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

gì được thời gian chạy .NET thực sự làm gì ở đây? Nó có phân tích cú pháp và chuyển đổi các thuộc tính thành các số nguyên trong 100 lần hay đủ thông minh để tìm ra rằng nó nên đệm các giá trị được phân tích cú pháp và không lặp lại tính toán cho từng phần tử trong phạm vi không?

Hơn nữa, làm thế nào tôi có thể tự tìm ra một thứ như thế này?

Cảm ơn trước sự giúp đỡ của bạn.

+2

"làm thế nào tôi có thể tự mình tìm ra một thứ như thế này" - cách tốt nhất là nghiên cứu IL được tạo ra từ mã này. – Andrey

+1

Bạn có thể đặt điểm ngắt trình gỡ rối trên phương thức Parse() và xem tần suất truy cập. –

Trả lời

2

Đã một lúc kể từ khi tôi đào mã này nhưng, IIRC, cách hoạt động Select là chỉ cần lưu trữ Func bạn cung cấp và chạy nó trên bộ sưu tập nguồn tại một thời điểm. Vì vậy, đối với mỗi phần tử trong phạm vi bên ngoài, nó sẽ chạy chuỗi bên trong Select/Aggregate như thể đây là lần đầu tiên. Không có bất kỳ bộ nhớ đệm tích hợp nào đang diễn ra - bạn sẽ phải tự thực hiện bộ nhớ đó trong các biểu thức.

Nếu bạn muốn con số này ra cho mình, bạn đã có ba tùy chọn cơ bản:

  1. Biên dịch mã và sử dụng ildasm để xem các IL; đó là chính xác nhất nhưng, đặc biệt là với lambdas và đóng cửa, những gì bạn nhận được từ IL có thể trông giống như những gì bạn đưa vào trình biên dịch C#.
  2. Sử dụng một cái gì đó như dotPeek để dịch ngược System.Linq.dll thành C#; một lần nữa, những gì bạn nhận được từ các loại công cụ này chỉ có thể gần giống với mã nguồn ban đầu, nhưng ít nhất nó sẽ là C# (và dotPeek nói riêng là một công việc khá tốt, và hoàn toàn miễn phí.)
  3. Sở thích cá nhân của tôi - tải xuống .NET 4.0 Reference Source và tìm kiếm chính bạn; Đây là những gì nó cho :) Bạn phải chỉ tin tưởng MS rằng nguồn tham chiếu phù hợp với nguồn thực tế được sử dụng để sản xuất các tập tin nhị phân, nhưng tôi không thấy bất kỳ lý do chính đáng để nghi ngờ họ.
  4. Như được chỉ ra bởi @AllonGuralnek bạn có thể đặt các điểm ngắt trên các biểu thức lambda cụ thể trong một dòng; đặt con trỏ của bạn một nơi nào đó bên trong cơ thể của lambda và nhấn F9 và nó sẽ phá vỡ chỉ lambda. (Nếu bạn làm điều đó sai, nó sẽ làm nổi bật toàn bộ dòng trong màu breakpoint, nếu bạn làm điều đó đúng, nó sẽ chỉ làm nổi bật lambda.)
+0

Cảm ơn bạn đã phản hồi. Tôi sẽ thử phương pháp thứ nhất và thứ ba. – Shredderroy

+2

4. Đặt con trỏ của bạn sau '=>' và nhấn F9. Điều đó sẽ đặt một điểm ngắt bên trong lambda và phá vỡ khi nó đạt đến nó. Lặp lại cho mỗi lambda và bạn có được một dấu vết tốt đẹp của những gì được gọi là khi nào. –

+0

@AllonGuralnek đó là một điểm tốt, tôi có xu hướng quên về lambdas breakpointing bởi vì tôi thường sử dụng chuột để thiết lập chúng :) –

4

LINQ và IEnumerable<T>pull. Điều này có nghĩa là các biến vị ngữ và hành động là một phần của câu lệnh LINQ nói chung không được thực thi cho đến khi các giá trị được kéo. Ngoài ra, các biến vị ngữ và hành động sẽ thực hiện mỗi giá trị thời gian được kéo (ví dụ: không có bộ nhớ đệm bí mật nào xảy ra).

kéo từ một IEnumerable<T> được thực hiện bởi các tuyên bố foreach mà thực sự là cú pháp đường để nhận một Enumerator bằng cách gọi IEnumerable<T>.GetEnumerator() và liên tục gọi IEnumerator<T>.MoveNext() để kéo các giá trị.

khai thác LINQ như ToList(), ToArray(), ToDictionary()ToLookup() kết thúc tốt đẹp một tuyên bố foreach do đó, những phương pháp này sẽ làm một kéo. Điều tương tự cũng có thể được nói về các toán tử như Aggregate(), Count()First(). Những phương pháp này có điểm chung là chúng tạo ra một kết quả duy nhất mà phải được tạo ra bằng cách thực hiện câu lệnh foreach.

Nhiều toán tử LINQ tạo một chuỗi IEnumerable<T> mới. Khi một phần tử được kéo ra khỏi chuỗi kết quả, toán tử sẽ lấy một hoặc nhiều phần tử từ chuỗi nguồn. Toán tử Select() là ví dụ rõ ràng nhất nhưng các ví dụ khác là SelectMany(), Where(), Concat(), Union(), Distinct(), Skip()Take(). Các toán tử này không thực hiện bất kỳ bộ nhớ đệm nào. Khi phần tử N'th được kéo từ Select(), nó kéo phần tử thứ N từ trình tự nguồn, áp dụng phép chiếu bằng cách sử dụng hành động được cung cấp và trả về nó. Không có gì bí mật xảy ra ở đây.

Các toán tử LINQ khác cũng tạo ra các chuỗi IEnumerable<T> mới nhưng chúng được thực hiện bằng cách thực sự kéo toàn bộ chuỗi nguồn, thực hiện công việc của chúng và sau đó tạo chuỗi mới. Các phương pháp này bao gồm Reverse(), OrderBy()GroupBy(). Tuy nhiên, thao tác kéo được thực hiện bởi toán tử chỉ được thực hiện khi chính toán tử được kéo có nghĩa là bạn vẫn cần một vòng lặp foreach "ở cuối" của câu lệnh LINQ trước khi bất cứ điều gì được thực thi. Bạn có thể lập luận rằng các toán tử này sử dụng bộ nhớ cache vì chúng ngay lập tức kéo toàn bộ chuỗi nguồn. Tuy nhiên, cache này được xây dựng mỗi khi toán tử được lặp lại vì vậy nó thực sự là một chi tiết thực hiện và không phải là một cái gì đó kỳ diệu sẽ phát hiện rằng bạn đang áp dụng cùng một thao tác OrderBy() nhiều lần cho cùng một trình tự.


Trong ví dụ của bạn, ToList() sẽ thực hiện thao tác kéo. Hành động ở bên ngoài Select sẽ thực hiện 100 lần. Mỗi khi hành động này được thực thi, Aggregate() sẽ thực hiện một thao tác kéo khác sẽ phân tích các thuộc tính XML. Trong tổng số mã của bạn sẽ gọi Int32.Parse() 200 lần.

Bạn có thể cải thiện điều này bằng cách kéo các thuộc tính một lần thay vì trên mỗi lần lặp:

var X = XElement.Parse (@" 
    <ROOT> 
     <MUL v='2' /> 
     <MUL v='3' /> 
    </ROOT> 
") 
.Elements() 
.Select (t => Int32.Parse (t.Attribute ("v").Value)) 
.ToList(); 
Enumerable.Range (1, 100) 
    .Select (s => x.Aggregate (s, (t, u) => t * u)) 
    .ToList() 
    .ForEach (s => Console.WriteLine (s)); 

Bây giờ Int32.Parse() chỉ được gọi 2 lần. Tuy nhiên, chi phí là một danh sách các giá trị thuộc tính phải được cấp phát, lưu trữ và cuối cùng là rác được thu thập. (Không phải là mối quan tâm lớn khi danh sách chứa hai phần tử.)

Lưu ý rằng nếu bạn quên ToList() đầu tiên sẽ kéo các thuộc tính, mã sẽ chạy nhưng với các đặc tính hiệu suất giống hệt như mã gốc. Không có không gian được sử dụng để lưu trữ các thuộc tính nhưng chúng được phân tích cú pháp trên mỗi lần lặp.

+0

Cảm ơn bạn rất nhiều vì đã trả lời chi tiết. – Shredderroy