2016-05-22 11 views
7

Tôi hiện đang tối ưu hóa thư viện cấp thấp và đã tìm thấy trường hợp phản trực giác. Cam kết gây ra câu hỏi này là here.C# Tại sao sử dụng phương pháp thể hiện làm đại biểu phân bổ các đối tượng tạm thời GC0 nhưng nhanh hơn 10% so với đại biểu được lưu trong bộ nhớ cache

Có một đại biểu

public delegate void FragmentHandler(UnsafeBuffer buffer, int offset, int length, Header header); 

và một phương pháp dụ

public void OnFragment(IDirectBuffer buffer, int offset, int length, Header header) 
{ 
    _totalBytes.Set(_totalBytes.Get() + length); 
} 

On this line, nếu tôi sử dụng phương pháp này như một đại biểu, chương trình phân bổ nhiều GC0 cho các đại biểu wrapper temp, nhưng hiệu suất nhanh hơn 10% (nhưng không ổn định).

var fragmentsRead = image.Poll(OnFragment, MessageCountLimit); 

Nếu tôi thay vì bộ nhớ cache phương pháp trong một đại biểu bên ngoài vòng lặp như thế này:

FragmentHandler onFragmentHandler = OnFragment; 

sau đó chương trình không phân bổ ở tất cả, con số này là rất ổn định nhưng chậm hơn nhiều.

Tôi đã xem qua IL được tạo và nó đang thực hiện tương tự, nhưng trong trường hợp sau newobj được gọi chỉ một lần và sau đó biến cục bộ nếu được tải.

Với cache IL_0034 đại biểu:

IL_002d: ldarg.0 
IL_002e: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header) 
IL_0034: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int) 
IL_0039: stloc.3 
IL_003a: br.s IL_005a 
// loop start (head: IL_005a) 
    IL_003c: ldloc.0 
    IL_003d: ldloc.3 
    IL_003e: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit 
    IL_0043: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32) 
    IL_0048: stloc.s fragmentsRead 

Với phân bổ tạm IL_0037:

IL_002c: stloc.2 
IL_002d: br.s IL_0058 
// loop start (head: IL_0058) 
    IL_002f: ldloc.0 
    IL_0030: ldarg.0 
    IL_0031: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header) 
    IL_0037: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int) 
    IL_003c: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit 
    IL_0041: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32) 
    IL_0046: stloc.s fragmentsRead 

Tại sao mã với phân bổ là nhanh hơn ở đây? Điều gì là cần thiết để tránh phân bổ nhưng giữ hiệu suất?

(thử nghiệm trên NET 4.5.2/4.6.1, x64, phát hành, trên hai máy khác nhau)

Cập nhật

Dưới đây là ví dụ độc lập mà ứng xử như mong đợi: đại biểu lưu trữ thực hiện hơn nhanh hơn 2 lần với 4 giây và 11 giây. Vì vậy, câu hỏi là cụ thể cho các dự án tham chiếu - những vấn đề tinh tế với trình biên dịch JIT hoặc cái gì khác có thể gây ra kết quả bất ngờ?

using System; 
using System.Diagnostics; 

namespace TestCachedDelegate { 

    public delegate int TestDelegate(int first, int second); 

    public static class Program { 
     static void Main(string[] args) 
     { 
      var tc = new TestClass(); 
      tc.Run(); 
     } 

     public class TestClass { 

      public void Run() { 
       var sw = new Stopwatch(); 
       sw.Restart(); 
       for (int i = 0; i < 1000000000; i++) { 
        CallDelegate(Add, i, i); 
       } 
       sw.Stop(); 
       Console.WriteLine("Non-cached: " + sw.ElapsedMilliseconds); 
       sw.Restart(); 
       TestDelegate dlgCached = Add; 
       for (int i = 0; i < 1000000000; i++) { 
        CallDelegate(dlgCached, i, i); 
       } 
       sw.Stop(); 
       Console.WriteLine("Cached: " + sw.ElapsedMilliseconds); 
       Console.ReadLine(); 
      } 

      public int CallDelegate(TestDelegate dlg, int first, int second) { 
       return dlg(first, second); 
      } 

      public int Add(int first, int second) { 
       return first + second; 
      } 

     } 
    } 
} 
+2

tôi đề nghị bạn viết [MCVE] của vấn đề của bạn mà không liên quan đến mã cụ thể tên miền của bạn, vì vậy điều này có thể được sao chép trên các máy khác. –

+1

Cam kết của bạn hơi không rõ ràng, giới thiệu một biến có cùng tên với trường lớp ngoại trừ dấu gạch dưới hàng đầu và phiên bản cũ đề cập đến trường lớp. Bạn chắc chắn bạn đã thử nghiệm với biến địa phương, tôi hy vọng? – hvd

+0

@hvd Biến cục bộ bên trong vòng lặp hoạt động giống như sử dụng phương thức bên trong hàm '.Poll()'. Biến cục bộ bên ngoài vòng lặp hoạt động tương tự như một trường. –

Trả lời

2

Vì vậy, sau khi đọc câu hỏi quá nhanh và nghĩ rằng nó đã hỏi một điều gì khác tôi cuối cùng đã có thời gian ngồi xuống và chơi thử Aeoron được đề cập.

Tôi đã thử một vài điều, trước hết tôi so sánh IL và Assembler được sản xuất và thấy rằng về cơ bản không có sự khác biệt tại trang web nơi chúng tôi gọi Poll() hoặc tại trang nơi trình xử lý thực sự được gọi.

Thứ hai, tôi đã thử nhận xét mã trong phương thức Poll() để xác nhận rằng phiên bản đã lưu trong bộ nhớ cache thực sự chạy nhanh hơn (phiên bản đó đã làm). Tôi đã thử nhìn vào các bộ đếm CPU (Cache nhớ, hướng dẫn đã nghỉ hưu và dự đoán sai chi nhánh) trong lược tả VS nhưng không thể thấy bất kỳ sự khác biệt nào giữa hai phiên bản khác với thực tế là hàm tạo của đại biểu rõ ràng được gọi nhiều hơn lần.

Điều này khiến tôi suy nghĩ về trường hợp tương tự mà chúng tôi chạy trong khi đang ở chế độ Disruptor-net nơi chúng tôi có thử nghiệm chạy chậm hơn phiên bản java nhưng chúng tôi chắc chắn chúng tôi không làm gì tốn kém hơn. Lý do cho "chậm chạp" của thử nghiệm là chúng tôi đã thực sự nhanh hơn và do đó theo lô ít hơn và do đó thông lượng của chúng tôi thấp hơn.

Nếu bạn chèn Thread.SpinWait (5) ngay trước cuộc gọi đến Poll(), bạn sẽ thấy hiệu suất tương tự hoặc tốt hơn như phiên bản không được lưu trong bộ nhớ cache.

câu trả lời gốc cho câu hỏi mà tôi nghĩ vào thời điểm đó đã được "tại sao sử dụng một đại biểu phương pháp dụ là chậm hơn so với bộ nhớ đệm các đại biểu bằng tay":

Các đầu mối là trong câu hỏi. Đó là một phương thức thể hiện và do đó nó nắm bắt hoàn toàn thành viên this và thực tế rằng điều này được chụp có nghĩa là nó không thể được lưu trữ. Do đó this sẽ không bao giờ thay đổi trong suốt thời gian tồn tại của đại biểu được lưu trong bộ nhớ cache, nên có thể lưu vào bộ nhớ cache.

Nếu bạn mở rộng nhóm phương pháp thành (first, second) => this.Add(first, second), việc chụp trở nên rõ ràng hơn.

Lưu ý rằng đội Roslyn đang nghiên cứu sửa chữa này: https://github.com/dotnet/roslyn/issues/5835

+0

Cách này trả lời câu hỏi này !? –

+0

@ V.B. Tôi đã không đọc câu hỏi đúng, tôi đã cập nhật câu trả lời của mình. – Slugart

+0

Cảm ơn bạn! Rất thú vị –

Các vấn đề liên quan