2016-05-22 41 views
7

我目前優化低級別的圖書館,找到了一個反直覺的情況下快10%。導致此問題的提交是hereC#爲什麼使用實例方法委託分配GC0臨時對象,但不是緩存委託

有一個代表

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

和實例方法

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

this line,如果我使用的方法作爲代表,該程序分配的臨時委託包裝許多GC0,但性能提高10%(但不穩定)。

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

如果我不是在緩存外循環委託這樣的方法:

FragmentHandler onFragmentHandler = OnFragment; 

則程序不分配在所有的,數字是很穩定,但慢得多。

我通過產生IL一看,這是做同樣的事情,但在後一種情況下,如果newobj裝入只調用一次,然後局部變量。

使用緩存的委託IL_0034:

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 

隨着溫度的分配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 

爲什麼與分配的代碼是更快嗎?需要什麼來避免分配,但保持表現?

(上.NET 4.5.2/4.6.1,64,推出,在兩個不同的機器上測試)

更新

下面是獨立的例子,它相當於預期:緩存委託執行更比4秒和11秒快兩倍。所以這個問題是特定於被引用的項目的 - JIT編譯器或其他什麼可能會導致意外結果的細微問題?

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

我建議您發佈的問題,就是不相關的領域特定代碼的[MCVE],所以這可以在其他機器上覆制。 –

+1

您的提交有點不清楚,引入了一個與類字段名稱相同的變量,除了前導下劃線以及引用類字段的舊版本。你確定你已經用局部變量進行了測試,我希望? – hvd

+0

@hvd循環內的局部變量的行爲與使用'.Poll()'函數內的方法相同。循環外部的局部變量的行爲與字段類似。 –

回答

2

所以太快閱讀的問題,並思考它問別的,我終於有一些時間坐下來與有關Aeoron測試玩了。

我嘗試了幾件事情,首先我比較了IL和Assembler的生產過程,發現在我們稱之爲Poll()的站點或者實際調用處理程序的站點上基本沒有區別。

其次我試圖在Poll()方法註釋掉的代碼,以確認緩存版本並實際運行速度更快(它沒有)。我試着在VS探查器中查看CPU計數器(緩存未命中,指令退出和分支預測錯誤),但看不到兩個版本之間的任何差異,除了委託構造函數顯然被稱爲更多倍。

這讓我想到了一個類似的案例,我們在移植Disruptor-net時遇到了一個比java版本運行慢的測試,但我們確信我們沒有做任何更昂貴的事情。測試「緩慢」的原因是我們實際上速度更快,因此批次更少,因此吞吐量更低。

如果您在調用Poll()之前插入Thread.SpinWait(5),您將看到與非緩存版本相同或更好的性能。

原來的答覆,而我認爲當時的問題是「爲什麼用一個實例方法委託是比手動緩存委託慢」:

線索是問題。這是一種實例方法,因此它隱式捕獲成員,並且捕獲這個事實意味着它不能被緩存。鑑於this在緩存委託的生命週期內永遠不會更改,因此它應該可緩存。

如果將方法組擴展爲,則捕獲變得更加明顯。

注意,羅斯林團隊正在努力解決這個:https://github.com/dotnet/roslyn/issues/5835

+0

這是如何回答這個問題的! –

+0

@ V.B。我沒有正確地閱讀這個問題,我已經更新了我的答案。 – Slugart

+0

謝謝!很有意思 –