2010-01-17 3 views
49

Làm theo câu hỏi này - Pass Method as Parameter using C# và một số kinh nghiệm cá nhân của tôi tôi muốn biết thêm một chút về hiệu suất gọi điện thoại cho đại biểu so với chỉ gọi một phương thức trong C#.Hiệu suất gọi các đại biểu so với các phương thức

Mặc dù đại biểu vô cùng thuận tiện, tôi đã có một ứng dụng thực hiện rất nhiều cuộc gọi lại qua đại biểu và khi chúng tôi viết lại điều này để sử dụng giao diện gọi lại, chúng tôi đã có được thứ tự cải thiện tốc độ. Điều này là với NET 2.0 vì vậy tôi không chắc chắn làm thế nào mọi thứ đã thay đổi với 3 và 4.

Các cuộc gọi đến các đại biểu được xử lý nội bộ trong trình biên dịch/CLR như thế nào và điều này ảnh hưởng đến hiệu suất của các cuộc gọi phương thức?


EDIT - Để làm rõ những gì tôi có ý nghĩa của các đại biểu vs giao diện gọi lại.

Đối với các cuộc gọi không đồng bộ, lớp học của tôi có thể cung cấp sự kiện OnComplete và đại biểu được liên kết mà người gọi có thể đăng ký. Ngoài ra, tôi có thể tạo ra một giao diện ICallback với một phương thức OnComplete mà người gọi thực hiện và sau đó đăng ký với lớp mà sau đó sẽ gọi phương thức đó khi hoàn thành (tức là cách Java xử lý những thứ này).

+1

Tôi không rõ ràng về những gì bạn đang yêu cầu ... callbacks giao diện _are_ đại biểu. –

+1

Xem http://stackoverflow.com/questions/1269452/is-using-delegates-excessively-a-bad-idea-for-performance và http://stackoverflow.com/questions/304770/does-using-delegates- các chương trình chạy chậm-mạng-của tôi - các bản sao có thể có? – itowlson

+0

đại biểu là cần thiết nếu bạn đang chạy chủ đề riêng biệt và cần phải giao diện với các chủ đề giao diện người dùng .. vì vậy bạn cần phải tinh chỉnh câu hỏi của bạn nhiều hơn để được bản địa hóa hơn và ít chung chung hơn. –

Trả lời

66

Tôi chưa thấy hiệu ứng đó - tôi chắc chắn chưa bao giờ gặp phải vấn đề này là nút cổ chai.

Dưới đây là một chuẩn mực rất thô và sẵn sàng trong đó cho thấy (trên hộp của tôi anyway) đại biểu thực sự là nhanh hơn giao diện:

using System; 
using System.Diagnostics; 

interface IFoo 
{ 
    int Foo(int x); 
} 

class Program : IFoo 
{ 
    const int Iterations = 1000000000; 

    public int Foo(int x) 
    { 
     return x * 3; 
    } 

    static void Main(string[] args) 
    { 
     int x = 3; 
     IFoo ifoo = new Program(); 
     Func<int, int> del = ifoo.Foo; 
     // Make sure everything's JITted: 
     ifoo.Foo(3); 
     del(3); 

     Stopwatch sw = Stopwatch.StartNew();   
     for (int i = 0; i < Iterations; i++) 
     { 
      x = ifoo.Foo(x); 
     } 
     sw.Stop(); 
     Console.WriteLine("Interface: {0}", sw.ElapsedMilliseconds); 

     x = 3; 
     sw = Stopwatch.StartNew();   
     for (int i = 0; i < Iterations; i++) 
     { 
      x = del(x); 
     } 
     sw.Stop(); 
     Console.WriteLine("Delegate: {0}", sw.ElapsedMilliseconds); 
    } 
} 

Kết quả (.NET 3.5; .NET 4.0b2 là về giống nhau):

Interface: 5068 
Delegate: 4404 

Bây giờ tôi không có niềm tin cụ thể mà có nghĩa là đại biểu là thực sự nhanh hơn so với giao diện ... nhưng nó làm cho tôi khá thuyết phục rằng họ không phải là một thứ tự o f cường độ chậm hơn. Ngoài ra, điều này đang làm hầu như không có gì trong phương thức đại biểu/giao diện. Rõ ràng chi phí gọi là sẽ làm cho sự khác biệt ít hơn và ít hơn khi bạn làm nhiều hơn và nhiều hơn nữa công việc cho mỗi cuộc gọi.

Một điều cần lưu ý là bạn không tạo đại biểu mới nhiều lần trong trường hợp bạn chỉ sử dụng một giao diện duy nhất. có thể gây ra vấn đề vì nó sẽ kích hoạt thu gom rác vv Nếu bạn đang sử dụng phương thức thể hiện làm đại biểu trong vòng lặp, bạn sẽ thấy hiệu quả hơn khi khai báo biến đại biểu bên ngoài vòng lặp và sử dụng lại nó. Ví dụ:

Func<int, int> del = myInstance.MyMethod; 
for (int i = 0; i < 100000; i++) 
{ 
    MethodTakingFunc(del); 
} 

là hiệu quả hơn:

for (int i = 0; i < 100000; i++) 
{ 
    MethodTakingFunc(myInstance.MyMethod); 
} 

thể này đã được các vấn đề bạn đang nhìn thấy?

+1

Bạn có thể giải thích về những gì trình biên dịch thực hiện trong trường hợp cuối cùng không? Liệu nó có tạo ra một cá thể ủy nhiệm mới trên mỗi lần lặp hay không? – Jan

+2

Thay đổi này có thể thay đổi nếu bạn chuyển nó thành sự kiện bằng cách sử dụng ủy nhiệm không? –

+0

@JanJ: Kiểm tra mã được biên dịch với ildasm, nhưng tôi tin rằng nó sẽ có. @ Chris S: Bạn có thể cung cấp thêm chi tiết về ý của bạn không? –

19

Vì CLR v 2, chi phí gọi của đại biểu rất gần với lời gọi phương thức ảo, được sử dụng cho các phương thức giao diện.

Xem blog của Joel Pobar.

+0

Cảm ơn, một số thông tin hữu ích ở đó. – Paolo

15

Tôi thấy hoàn toàn không thể tin được rằng đại biểu nhanh hơn hoặc chậm hơn đáng kể so với phương pháp ảo. Nếu bất cứ điều gì các đại biểu nên được negligibly nhanh hơn. Ở mức độ thấp hơn, các đại biểu thường được thực hiện một cái gì đó tương tự (sử dụng ký hiệu C-phong cách, nhưng xin vui lòng tha thứ cho bất kỳ lỗi cú pháp nhỏ như thế này chỉ là một minh họa):

struct Delegate { 
    void* contextPointer; // What class instance does this reference? 
    void* functionPointer; // What method does this reference? 
} 

Gọi một đại biểu làm việc gì đó như:

struct Delegate myDelegate = somethingThatReturnsDelegate(); 
// Call the delegate in de-sugared C-style notation. 
ReturnType returnValue = 
    (*((FunctionType) *myDelegate.functionPointer))(myDelegate.contextPointer); 

một lớp học, dịch sang C, sẽ là một cái gì đó như:

struct SomeClass { 
    void** vtable;  // Array of pointers to functions. 
    SomeType someMember; // Member variables. 
} 

để gọi một hàm vritual, bạn sẽ làm như sau:

struct SomeClass *myClass = someFunctionThatReturnsMyClassPointer(); 
// Call the virtual function residing in the second slot of the vtable. 
void* funcPtr = (myClass -> vtbl)[1]; 
ReturnType returnValue = (*((FunctionType) funcPtr))(myClass); 

Về cơ bản chúng giống nhau, ngoại trừ khi sử dụng các hàm ảo, bạn sẽ trải qua một lớp bổ sung để lấy con trỏ hàm. Tuy nhiên, lớp bổ sung thêm này thường là miễn phí vì các bộ dự đoán nhánh CPU hiện đại sẽ đoán địa chỉ của con trỏ hàm và thực thi mục tiêu của nó song song với việc tìm kiếm địa chỉ của hàm. Tôi đã tìm thấy (mặc dù trong D, không phải C#) rằng các cuộc gọi hàm ảo trong một vòng lặp chặt chẽ không chậm hơn so với các cuộc gọi trực tiếp không được chỉ định, miễn là cho bất kỳ lần chạy nào của vòng lặp mà chúng luôn phân giải thành cùng một hàm thực .

+1

Đó luôn là giả định của tôi cho đến khi tôi bắt gặp sự bất thường mà tôi đã mô tả trong câu hỏi. Có lẽ như Jon cho thấy một cái gì đó khác là vấn đề và tôi đã bị mắc kẹt trên một "đại biểu chậm hơn" meme do nhầm lẫn. – Paolo

1

Điều gì về thực tế là đại biểu là các thùng chứa? Không phải là khả năng multicast thêm chi phí? Trong khi chúng ta đang ở trên chủ đề, điều gì sẽ xảy ra nếu chúng ta đẩy khía cạnh container này thêm một chút? Không có gì cấm chúng tôi, nếu d là một đại biểu, từ thực hiện d + = d; hoặc từ việc xây dựng một đồ thị được chỉ định phức tạp tùy ý (cặp con trỏ ngữ cảnh, phương thức con trỏ). Tôi có thể tìm tài liệu mô tả cách biểu đồ này được truyền qua khi nào đại biểu được gọi?

+0

Về mặt khái niệm, không có lý do tại sao sự hỗ trợ cho các đại biểu multicast sẽ phải làm chậm sự kêu gọi trong trường hợp đích đơn. Nếu các đại biểu có nhiều mục tiêu đặt con trỏ phương thức nội bộ của họ thành một phương thức ExecuteMultiDelegate đặc biệt và tham chiếu đích bên trong của nó thành một mảng các cặp tổ chức (Object, Method), các đại biểu có thể gửi không điều kiện đến phương thức của họ mà không kiểm tra xem có nhiều mục tiêu hay không. Phương thức ExecuteMultiDelegate sẽ phải có một số hành vi kiểm tra kiểu bình thường bị vô hiệu hóa, nhưng điều đó có thể thực hiện được. – supercat

+0

Lưu ý rằng cách tiếp cận mà tôi vừa mô tả không phải là AFAIK cách MulticastDelegates thực sự được triển khai thực hiện, nhưng nó sẽ là một cơ chế để tối ưu hóa trường hợp phổ biến nhất (đích xác là một đích). – supercat

4

Tôi đã thực hiện một số kiểm tra (trong .Net 3.5 ... sau này tôi sẽ kiểm tra tại nhà bằng cách sử dụng .Net 4). Thực tế là: Nhận đối tượng dưới dạng giao diện và sau đó thực hiện phương thức nhanh hơn nhận đại biểu từ phương thức, sau đó gọi cho đại biểu.

Xem xét biến đã ở đúng loại (giao diện hoặc đại biểu) và đơn giản gọi nó làm cho đại biểu giành chiến thắng.

Vì lý do nào đó, việc ủy ​​quyền qua phương thức giao diện (có thể trên bất kỳ phương thức ảo nào) sẽ chậm hơn. Và, xem xét có những trường hợp khi chúng tôi đơn giản không thể lưu trữ trước đại biểu (như trong Dispatches, ví dụ), có thể biện minh cho lý do tại sao giao diện nhanh hơn.

Dưới đây là kết quả:

Để có được kết quả thực sự, biên dịch này trong chế độ Release và chạy nó bên ngoài Visual Studio.

Kiểm tra các cuộc gọi trực tiếp hai lần
00: 00: 00.5834988
00: 00: 00,5997071

Kiểm tra cuộc gọi giao diện, nhận giao diện tại mỗi cuộc gọi
00: 00: 05,8998212

Kiểm tra cuộc gọi giao diện, nhận giao diện một lần
00:00:05.3163224

Kiểm tra Action (đại biểu) gọi, nhận được hành động ở mọi cuộc gọi
00: 00: 17,1807980

Kiểm tra Action (đại biểu) gọi, nhận được hành động một lần
00: 00: 05,3163224

Kiểm tra Action (đại biểu) trên một phương pháp giao diện, nhận cả ở mỗi cuộc gọi
00: 03: 50,7326056

Kiểm tra Action (đại biểu) trên một n phương pháp giao diện, nhận giao diện một lần, các đại biểu tại mỗi cuộc gọi
00: 03: 48,9141438

Kiểm tra Action (đại biểu) trên một phương pháp giao diện, nhận được cả hai một lần
00: 00: 04,0036530

Như bạn có thể thấy, các cuộc gọi trực tiếp thực sự nhanh chóng. Lưu trữ giao diện hoặc ủy quyền trước đó, và sau đó chỉ gọi nó là rất nhanh. Nhưng phải có được một đại biểu là chậm hơn so với việc phải có được một giao diện. Có để có được một đại biểu trên một phương pháp giao diện (hoặc phương pháp ảo, không chắc chắn) là thực sự chậm (so sánh 5 giây nhận được một đối tượng như một giao diện đến gần 4 phút làm như vậy để có được hành động).

Các mã mà tạo ra những kết quả là đây:

using System; 

namespace ActionVersusInterface 
{ 
    public interface IRunnable 
    { 
     void Run(); 
    } 
    public sealed class Runnable: 
     IRunnable 
    { 
     public void Run() 
     { 
     } 
    } 

    class Program 
    { 
     private const int COUNT = 1700000000; 
     static void Main(string[] args) 
     { 
      var r = new Runnable(); 

      Console.WriteLine("To get real results, compile this in Release mode and"); 
      Console.WriteLine("run it outside Visual Studio."); 

      Console.WriteLine(); 
      Console.WriteLine("Checking direct calls twice"); 
      { 
       DateTime begin = DateTime.Now; 
       for (int i = 0; i < COUNT; i++) 
       { 
        r.Run(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 
      { 
       DateTime begin = DateTime.Now; 
       for (int i = 0; i < COUNT; i++) 
       { 
        r.Run(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking interface calls, getting the interface at every call"); 
      { 
       DateTime begin = DateTime.Now; 
       for (int i = 0; i < COUNT; i++) 
       { 
        IRunnable interf = r; 
        interf.Run(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking interface calls, getting the interface once"); 
      { 
       DateTime begin = DateTime.Now; 
       IRunnable interf = r; 
       for (int i = 0; i < COUNT; i++) 
       { 
        interf.Run(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking Action (delegate) calls, getting the action at every call"); 
      { 
       DateTime begin = DateTime.Now; 
       for (int i = 0; i < COUNT; i++) 
       { 
        Action a = r.Run; 
        a(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking Action (delegate) calls, getting the Action once"); 
      { 
       DateTime begin = DateTime.Now; 
       Action a = r.Run; 
       for (int i = 0; i < COUNT; i++) 
       { 
        a(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 


      Console.WriteLine(); 
      Console.WriteLine("Checking Action (delegate) over an interface method, getting both at every call"); 
      { 
       DateTime begin = DateTime.Now; 
       for (int i = 0; i < COUNT; i++) 
       { 
        IRunnable interf = r; 
        Action a = interf.Run; 
        a(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking Action (delegate) over an interface method, getting the interface once, the delegate at every call"); 
      { 
       DateTime begin = DateTime.Now; 
       IRunnable interf = r; 
       for (int i = 0; i < COUNT; i++) 
       { 
        Action a = interf.Run; 
        a(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 

      Console.WriteLine(); 
      Console.WriteLine("Checking Action (delegate) over an interface method, getting both once"); 
      { 
       DateTime begin = DateTime.Now; 
       IRunnable interf = r; 
       Action a = interf.Run; 
       for (int i = 0; i < COUNT; i++) 
       { 
        a(); 
       } 
       DateTime end = DateTime.Now; 
       Console.WriteLine(end - begin); 
      } 
      Console.ReadLine(); 
     } 
    } 

} 
+2

Bạn có lẽ không nên đưa người được ủy nhiệm vào thời gian cần thiết để chạy nó. – TamusJRoyce

+5

Bạn cũng nên sử dụng [Lớp đồng hồ bấm giờ] (http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch (v = vs.110) .aspx) thay vì DateTime. Eric Lippert đi vào [chi tiết hơn] (http://tech.pro/tutorial/1295/c-performance-benchmark-mistakes-part-two). – FriendlyGuy