2009-08-28 27 views
34

Tôi đang xem xét sử dụng một cái gì đó như StackFrame stackFrame = new StackFrame(1) để đăng nhập phương thức thực thi, nhưng tôi không biết về ý nghĩa hiệu suất của nó. Là ngăn xếp dấu vết một cái gì đó được xây dựng anyway với mỗi cuộc gọi phương pháp để hiệu suất không phải là một mối quan tâm hoặc là nó cái gì đó chỉ xây dựng khi được yêu cầu? Bạn có khuyên bạn nên chống lại nó trong một ứng dụng mà hiệu suất là rất quan trọng? Nếu vậy, điều đó có nghĩa là tôi nên vô hiệu hóa nó cho bản phát hành?Hiệu suất của StackFrame như thế nào?

+3

Hãy coi chừng có một SO-bot trả lời tất cả các câu hỏi như sau: Hãy thử! – harpo

+0

Liệu nhật ký này có luôn được bật không? Cách tiếp cận thay thế là gì? Kết quả của Michael rất thú vị, nhưng câu hỏi thực sự là "cách tiếp cận này xếp chồng lên X như thế nào?" – STW

+0

@Yoooder: câu hỏi hay. Tôi nghĩ rằng "X" chúng tôi so sánh điều này là ghi nhật ký mà không có thông tin về phương thức hoặc với thông tin phương thức được tạo tĩnh. "Tôi có nên tắt tính năng này không?": Thử nghiệm trong ứng dụng của bạn và xem liệu sự khác biệt về hiệu suất có thể đo lường hay đáng chú ý với khối lượng công việc của bạn hay không. –

Trả lời

28

chỉnh sửa: Một số nền


Chúng tôi có một tính năng tương tự mà bị vô hiệu hóa 99% thời gian; chúng tôi đã sử dụng một cách tiếp cận như:

public void DoSomething() 
{ 
    TraceCall(MethodBase.GetCurrentMethod().Name); 
    // Do Something 
} 

public void TraceCall(string methodName) 
{ 
    if (!loggingEnabled) { return; } 
    // Log... 
} 

TraceCall(MethodBase.GetCurrentMethod().Name) 

Đó là đơn giản, nhưng bất kể có hay không truy tìm được kích hoạt, chúng tôi đã phải gánh chịu hiệu suất trúng của việc sử dụng Reflection để tra cứu tên phương pháp.

Tùy chọn của chúng tôi là yêu cầu mã nhiều hơn trong mọi phương pháp (và rủi ro đơn giản hoặc từ chối) hoặc chuyển sang sử dụng StackFrame để xác định phương thức gọi chỉ khi ghi nhật ký được bật.

Lựa chọn A:

public void DoSomething() 
{ 
    if (loggingEnabled) 
    { 
     TraceCall(MethodBase.GetCurrentMethod().Name); 
    } 
    // Do Something 
} 

public void TraceCall(string methodName) 
{ 
    if (!loggingEnabled) { return; } 
    // Log... 
} 

Lựa chọn B:

public void DoSomething() 
{ 
    TraceCall(); 
    // Do Something 
} 

public void TraceCall() 
{ 
    if (!loggingEnabled) { return; } 
    StackFrame stackFrame = new StackFrame(1); 
    // Log... 
} 

Chúng tôi đã lựa chọn Lựa chọn B. Nó cung cấp cải tiến hiệu suất đáng kể so với lựa chọn A khi đăng nhập bị vô hiệu hóa, 99% thời gian và rất đơn giản để triển khai.

Dưới đây là một thay đổi mã của Michael, để hiển thị các chi phí/lợi ích của phương pháp này

using System; 
using System.Diagnostics; 
using System.Reflection; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     static bool traceCalls; 

     static void Main(string[] args) 
     { 
      Stopwatch sw; 

      // warm up 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 

      // call 100K times, tracing *disabled*, passing method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = false; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(MethodBase.GetCurrentMethod()); 
      } 
      sw.Stop(); 
      Console.WriteLine("Tracing Disabled, passing Method Name: {0}ms" 
          , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *enabled*, passing method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = true; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(MethodBase.GetCurrentMethod()); 
      } 
      sw.Stop(); 
      Console.WriteLine("Tracing Enabled, passing Method Name: {0}ms" 
          , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *disabled*, determining method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = false; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 
      Console.WriteLine("Tracing Disabled, looking up Method Name: {0}ms" 
         , sw.ElapsedMilliseconds); 

      // call 100K times, tracing *enabled*, determining method name 
      sw = Stopwatch.StartNew(); 
      traceCalls = true; 
      for (int i = 0; i < 100000; i++) 
      { 
       TraceCall(); 
      } 
      Console.WriteLine("Tracing Enabled, looking up Method Name: {0}ms" 
         , sw.ElapsedMilliseconds); 

      Console.ReadKey(); 
     } 

     private static void TraceCall() 
     { 
      if (traceCalls) 
      { 
       StackFrame stackFrame = new StackFrame(1); 
       TraceCall(stackFrame.GetMethod().Name); 
      } 
     } 

     private static void TraceCall(MethodBase method) 
     { 
      if (traceCalls) 
      { 
       TraceCall(method.Name); 
      } 
     } 

     private static void TraceCall(string methodName) 
     { 
      // Write to log 
     } 
    } 
} 

Kết quả:

Tracing Disabled, passing Method Name: 294ms 
Tracing Enabled, passing Method Name: 298ms 
Tracing Disabled, looking up Method Name: 0ms 
Tracing Enabled, looking up Method Name: 1230ms 
+0

Điểm tuyệt vời: thông thường, bạn có thể lùi lại và tìm cách khác để giải quyết vấn đề. StackFrame() mới không đắt nếu bạn không bao giờ cần phải gọi nó. –

+0

@Michael: chính xác! nó rất sạch sẽ để thực hiện, dễ dàng để đơn vị kiểm tra, và thời gian duy nhất chúng tôi thực hiện hit hiệu suất là khi chúng tôi về cơ bản gỡ lỗi và hiệu suất không phải là một mối quan tâm anyways. – STW

+0

Tôi đoán nó phụ thuộc vào tần suất bạn truy tìm sẽ được bật sau đó vì tùy chọn B vẫn chậm hơn 10 lần so với tùy chọn A nếu truy tìm được bật. Nhưng bây giờ mọi người có thể tự quyết định dựa trên sự thật. Rất sâu sắc. Cảm ơn! –

5

Từ các tài liệu MSDN, nó có vẻ như StackFrames đang được tạo ra tất cả các thời gian:

Một StackFrame được tạo ra và đẩy vào các cuộc gọi stack cho mỗi cuộc gọi chức năng thực hiện trong việc thực hiện một chủ đề. Khung ngăn xếp luôn bao gồm Thông tin về MethodBase và tùy chọn bao gồm tên tệp, số dòng và thông tin về số cột.

Các constructor new StackFrame(1) bạn muốn gọi sẽ làm điều này:

private void BuildStackFrame(int skipFrames, bool fNeedFileInfo) 
{ 
    StackFrameHelper sfh = new StackFrameHelper(fNeedFileInfo, null); 
    StackTrace.GetStackFramesInternal(sfh, 0, null); 
    int numberOfFrames = sfh.GetNumberOfFrames(); 
    skipFrames += StackTrace.CalculateFramesToSkip(sfh, numberOfFrames); 
    if ((numberOfFrames - skipFrames) > 0) 
    { 
     this.method = sfh.GetMethodBase(skipFrames); 
     this.offset = sfh.GetOffset(skipFrames); 
     this.ILOffset = sfh.GetILOffset(skipFrames); 
     if (fNeedFileInfo) 
     { 
      this.strFileName = sfh.GetFilename(skipFrames); 
      this.iLineNumber = sfh.GetLineNumber(skipFrames); 
      this.iColumnNumber = sfh.GetColumnNumber(skipFrames); 
     } 
    } 
} 

GetStackFramesInternal là một phương pháp bên ngoài. CalculateFramesToSkip có một vòng lặp hoạt động chính xác một lần, vì bạn đã chỉ định 1 khung hình. Mọi thứ khác trông khá nhanh.

Bạn đã thử đo phải mất bao lâu để tạo ra, ví dụ, 1 triệu trong số đó?

+0

Cảm ơn Paul. Tôi đọc cùng một đoạn trong MSDN và cố gắng theo dõi cách nó hoạt động trong Reflector, nhưng không thực sự hiểu nó. Tôi đã nghĩ về việc định thời gian, nhưng nó có thể gây hiểu lầm nếu nó được lưu trữ bằng cách nào đó sau cuộc gọi đầu tiên đó là lý do tại sao tôi hy vọng ai đó biết nó hoạt động như thế nào trong nội bộ. –

+3

Kiểm tra của Michael cho thấy rằng các khung ngăn xếp không được "tạo và đẩy vào ngăn xếp cuộc gọi cho mọi cuộc gọi hàm được thực hiện trong quá trình thực thi một chuỗi" bởi vì nó mất nhiều thời gian hơn để yêu cầu. –

+4

Hoặc chúng có thể tạo chúng khác nhau hoặc có thể là cấu trúc gốc cho đến khi được yêu cầu hoặc tài liệu có thể sai. Tôi cho rằng CLR cần phải theo dõi ngăn xếp được quản lý, vì vậy phải có * cái gì đó * trong thiết lập bộ nhớ riêng khi chồng phát triển. –

16

Một kiểm tra nhanh chóng và ngây thơ chỉ ra rằng đối với mã hiệu suất nhạy cảm, vâng, bạn muốn chú ý đến điều này:

Đừng tạo 100K khung: 3ms

Tạo 100K khung: 1805ms

Khoảng 20 micro giây trên mỗi khung được tạo, trên máy của tôi. Không nhiều, nhưng một sự khác biệt có thể đo lường được qua một số lượng lớn các lần lặp lại.

Phát biểu với câu hỏi sau ("Tôi có nên tắt tính năng StackFrame trong ứng dụng của mình không?"), Tôi khuyên bạn nên phân tích ứng dụng của mình, thực hiện các kiểm tra hiệu suất như tôi đã thực hiện ở đây và xem hiệu suất khác biệt cho bất kỳ thứ gì với khối lượng công việc của bạn.

using System; 
using System.Diagnostics; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     static bool generateFrame; 

     static void Main(string[] args) 
     { 
      Stopwatch sw; 

      // warm up 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 

      // call 100K times; no stackframes 
      sw = Stopwatch.StartNew(); 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 
      sw.Stop(); 
      Console.WriteLine("Don't generate 100K frames: {0}ms" 
           , sw.ElapsedMilliseconds); 

      // call 100K times; generate stackframes 
      generateFrame = true; 
      sw = Stopwatch.StartNew(); 
      for (int i = 0; i < 100000; i++) 
      { 
       CallA(); 
      } 
      Console.WriteLine("Generate 100K frames: {0}ms" 
          , sw.ElapsedMilliseconds); 

      Console.ReadKey(); 
     } 

     private static void CallA() 
     { 
      CallB(); 
     } 

     private static void CallB() 
     { 
      CallC(); 
     } 

     private static void CallC() 
     { 
      if (generateFrame) 
      { 
       StackFrame stackFrame = new StackFrame(1); 
      } 
     } 
    } 
} 
+0

Cảm ơn Michael! Tôi thích cách bạn thử nghiệm này kể từ khi tôi đã quan tâm đến.NET khuôn khổ bằng cách nào đó bộ nhớ đệm phương pháp, nhưng thử nghiệm của bạn dường như cho thấy rằng đây không phải là trường hợp. –

+0

Vâng, tôi không chắc chắn làm thế nào để vuông bài Paul tham chiếu với kinh nghiệm và bài kiểm tra của tôi. Tôi cũng đã đi qua các tài liệu log4net, mà các cuộc gọi một cách rõ ràng gọi ra cách chậm tạo stackframes (để đưa vào các bản ghi) là. –

+0

Tôi lặp lại thử nghiệm này áp dụng [MethodImpl (MethodImplOptions.NoInlining)] cho mỗi phương pháp chỉ trong trường hợp và tôi cũng nhận được khoảng 20µs cho mỗi StackFrame với 100k lặp và khoảng 15µs cho mỗi StackFrame với 1 triệu lần lặp. 15-20 micro giây dường như không quá tệ. 50 Tác phẩm StackFrame chỉ mất 1 mili giây sau khi tất cả. –

4

Tôi đang xem xét sử dụng một cái gì đó giống như StackFrame StackFrame = new StackFrame (1) để đăng nhập phương thức thực hiện

Không quan tâm: Tại sao? Nếu bạn chỉ muốn phương pháp hiện tại, thì

string methodName = System.Reflection.MethodBase.GetCurrentMethod().Name; 

có vẻ tốt hơn. Có lẽ không thực hiện nhiều hơn (tôi không so sánh, nhưng Reflection cho thấy GetCurrentMethod() không đơn giản tạo ra một StackFrame nhưng lại có một số "ma thuật"), nhưng rõ ràng hơn trong ý định của nó.

+1

Tôi đã thử nghiệm của Michael với MethodBase.GetCurrentMethod() và bạn nói đúng, nó thực sự nhanh hơn. Mất khoảng 1 micro giây cho mỗi cuộc gọi trên máy của tôi. –

+0

Tôi chỉ nhìn thấy câu trả lời của yoooder, anh ấy làm cho một điểm tốt. –

2

Tôi nghĩ Paul Williams đánh vào đầu bằng công việc đang được thực hiện. Nếu bạn đào sâu hơn vào StackFrameHelper bạn sẽ thấy rằng fNeedFileInfo thực sự là kẻ giết người hiệu suất - đặc biệt là ở chế độ gỡ lỗi. Thử đặt nó false nếu hiệu suất là quan trọng. Bạn sẽ không nhận được nhiều thông tin hữu ích trong chế độ phát hành.

Nếu bạn vượt qua false tại đây bạn sẽ vẫn nhận được tên phương thức khi thực hiện ToString() và không xuất ra bất kỳ thông tin nào, chỉ cần di chuyển con trỏ ngăn xếp xung quanh, nó rất nhanh.

1

Tôi biết ý bạn là gì, nhưng kết quả ví dụ này vượt quá. Thực hiện GetCurrentMethod ngay cả khi đăng nhập được tắt là một sự lãng phí. Nó phải là một cái gì đó như:

if (loggingEnabled) TraceCall(MethodBase.GetCurrentMethod()); 

Hoặc nếu bạn muốn TraceCall luôn thực hiện:

TraceCall(loggingEnabled ? MethodBase.GetCurrentMethod() : null); 
10

Tôi biết đây là một bài cũ, nhưng chỉ trong trường hợp bất cứ ai đi qua nó ở đó là thay thế khác nếu bạn đang nhắm mục tiêu .Net 4.5

Bạn có thể sử dụng thuộc tính CallerMemberName để xác định tên phương thức gọi. Nó nhanh hơn nhiều so với phản xạ hoặc StackFrame. Đây là kết quả từ một bài kiểm tra nhanh lặp lại hàng triệu lần. StackFrame là cực kỳ chậm so với sự phản ánh, và thuộc tính mới làm cho cả hai trông giống như họ đang đứng yên. Điều này đã được chạy trong IDE.

Reflection Kết quả: 00: 00: 01,4098808

StackFrame quả 00:00:06.thuộc tính 2002501

CallerMemberName quả: 00: 00: 00,0042708

Xong

Sau đây là từ exe biên soạn: Reflection Kết quả: 00: 00: 01,2136738 StackFrame quả 00: 00: 03.6343924 CallerMemberName thuộc tính Kết quả: 00: 00: 00.0000947 Xong

 static void Main(string[] args) 
    { 

     Stopwatch sw = new Stopwatch(); 

     sw.Stop(); 

     Console.WriteLine("Reflection Result:"); 

     sw.Start(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      //Using reflection to get the current method name. 
      PassedName(MethodBase.GetCurrentMethod().Name); 
     } 
     Console.WriteLine(sw.Elapsed); 

     Console.WriteLine("StackFrame Result"); 

     sw.Restart(); 

     for (int i = 0; i < 1000000; i++) 
     { 
      UsingStackFrame(); 
     } 

     Console.WriteLine(sw.Elapsed); 

     Console.WriteLine("CallerMemberName attribute Result:"); 

     sw.Restart(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      UsingCallerAttribute(); 
     } 

     Console.WriteLine(sw.Elapsed); 

     sw.Stop(); 



     Console.WriteLine("Done"); 
     Console.Read(); 
    } 


    static void PassedName(string name) 
    { 

    } 

    static void UsingStackFrame() 
    { 
     string name = new StackFrame(1).GetMethod().Name; 
    } 


    static void UsingCallerAttribute([CallerMemberName] string memberName = "") 
    { 

    } 
+0

Lý do thuộc tính này nhanh hơn là do trình biên dịch thực sự ghi dữ liệu này vào hình ảnh kết quả trực tiếp. Trong tất cả những nơi mà một phương thức sử dụng các thuộc tính dịch vụ trình biên dịch được gọi, các giá trị được thay thế bằng bất kỳ trình biên dịch nào được yêu cầu cung cấp (CallerMemberName, CallerFileName, CallerLineNumber). Về cơ bản nó là viết lại nhị phân. Điều này có thể được xác nhận bằng cách sử dụng công cụ ILDASM. – PSGuy

+0

Điều này không nhận được loại của người gọi hoặc, mà rõ ràng là chỉ có sẵn thông qua một khung stack, và một phần thiết yếu của việc tìm ra nơi mà một cái gì đó đến từ. – StingyJack

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