2013-09-23 59 views
6

我有一個應用程序,我將日誌字符串保留在循環緩衝區中。當日志滿了時,對於每一個新的插入,舊的字符串將被釋放以進行垃圾收集,然後它們在第二代內存中。因此,最終會發生第二代GC,這是我想避免的。如何避免長生存的字符串導致第2代垃圾回收

我試圖將字符串編組爲一個結構。令人驚訝的是,我仍然獲得第二代GC:s。看來這個結構仍然保留對字符串的一些引用。下面完整的控制檯應用任何幫助讚賞。

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Runtime.InteropServices; 
using System.Text; 
using System.Threading.Tasks; 

namespace ConsoleApplication 
{ 
    class Program 
    { 

     [StructLayout(LayoutKind.Sequential)] 
     public struct FixedString 
     { 
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] 
      private string str; 

      public FixedString(string str) 
      { 
       this.str = str; 
      } 
     } 

     [StructLayout(LayoutKind.Sequential)] 
     public struct UTF8PackedString 
     { 
      private int length; 

      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)] 
      private byte[] str; 

      public UTF8PackedString(int length) 
      { 
       this.length = length; 
       str = new byte[length]; 
      } 

      public static implicit operator UTF8PackedString(string str) 
      { 
       var obj = new UTF8PackedString(Encoding.UTF8.GetByteCount(str)); 
       var bytes = Encoding.UTF8.GetBytes(str); 
       Array.Copy(bytes, obj.str, obj.length); 
       return obj; 
      } 
     } 

     const int BufferSize = 1000000; 
     const int LoopCount = 10000000; 

     static void Main(string[] args) 
     { 
      Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", 
       "Type".PadRight(20), "Time", "GC(0)", "GC(1)", "GC(2)"); 
      Console.WriteLine(); 
      for (int i = 0; i < 5; i++) 
      { 
       TestPerformance<string>(s => s); 
       TestPerformance<FixedString>(s => new FixedString(s)); 
       TestPerformance<UTF8PackedString>(s => s); 
       Console.WriteLine(); 
      } 
      Console.ReadKey(); 
     } 

     private static void TestPerformance<T>(Func<string, T> func) 
     { 
      var buffer = new T[BufferSize]; 
      GC.Collect(2); 
      Stopwatch stopWatch = new Stopwatch(); 
      var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) }; 
      stopWatch.Reset(); 
      stopWatch.Start(); 
      for (int i = 0; i < LoopCount; i++) 
       buffer[i % BufferSize] = func(i.ToString()); 
      stopWatch.Stop(); 
      Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", 
       typeof(T).Name.PadRight(20), 
       stopWatch.ElapsedMilliseconds, 
       (GC.CollectionCount(0) - initialCollectionCounts[0]), 
       (GC.CollectionCount(1) - initialCollectionCounts[1]), 
       (GC.CollectionCount(2) - initialCollectionCounts[2]) 
      ); 
     } 
    } 
} 

編輯:更新的代碼UnsafeFixedString,做必要的工作:在我的電腦上

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Runtime.InteropServices; 
using System.Text; 
using System.Threading.Tasks; 

namespace ConsoleApplication 
{ 
    class Program 
    { 
     public unsafe struct UnsafeFixedString 
     { 
      private int length; 

      private fixed char str[256]; 

      public UnsafeFixedString(int length) 
      { 
       this.length = length; 
      } 

      public static implicit operator UnsafeFixedString(string str) 
      { 
       var obj = new UnsafeFixedString(str.Length); 
       for (int i = 0; i < str.Length; i++) 
        obj.str[i] = str[i];     
       return obj; 
      } 
     } 

     const int BufferSize = 1000000; 
     const int LoopCount = 10000000; 

     static void Main(string[] args) 
     { 
      Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", 
       "Type".PadRight(20), "Time", "GC(0)", "GC(1)", "GC(2)"); 
      Console.WriteLine(); 
      for (int i = 0; i < 5; i++) 
      { 
       TestPerformance(s => s); 
       TestPerformance<UnsafeFixedString>(s => s); 
       Console.WriteLine(); 
      } 
      Console.ReadKey(); 
     } 

     private static void TestPerformance<T>(Func<string, T> func) 
     { 
      var buffer = new T[BufferSize]; 
      GC.Collect(2); 
      Stopwatch stopWatch = new Stopwatch(); 
      var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) }; 
      stopWatch.Reset(); 
      stopWatch.Start(); 
      for (int i = 0; i < LoopCount; i++) 
       buffer[i % BufferSize] = func(String.Format("{0}", i)); 
      stopWatch.Stop(); 
      Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", 
       typeof(T).Name.PadRight(20), 
       stopWatch.ElapsedMilliseconds, 
       (GC.CollectionCount(0) - initialCollectionCounts[0]), 
       (GC.CollectionCount(1) - initialCollectionCounts[1]), 
       (GC.CollectionCount(2) - initialCollectionCounts[2]) 
      ); 
     } 
    } 
} 

輸出是:

Type     Time GC(0) GC(1) GC(2) 

String     5746 160  71  19 
UnsafeFixedString  5345 418  0  0 
+0

爲什麼你想避免第2代垃圾回收? – PVitt

+0

該應用程序有一些實時要求。 GC(2)導致完全凍結。 –

+1

如果您將消息存儲爲byte [](encoding.utf8.getbytes()而不是字符串) –

回答

7

它不應該是一個驚喜,一個structstring字段在這裏做了區別:a string字段是總是只是一個參考ce到託管堆上的對象 - 具體而言,某個地方有一個string對象。 string仍然存在,最終仍會導致GC2。

「解決」這個唯一的方法是沒有它作爲對象在所有;並且只有這樣,才能做到這一點(不管理存儲器的外部完全去)是使用fixed緩衝液:

public unsafe struct FixedString 
{ 
    private fixed char str[100]; 
} 

在這裏,每結構實例FixedString具有用於數據保留的200個字節。 str只是char*的相對偏移量,表示此預訂的開始。然而,工作與此相當棘手 - 並要求整個代碼unsafe。另外請注意,無論您實際上是要存儲3個字符還是170個,每個FixedString都保留相同的空間量。爲避免出現內存問題,您需要使用空間調整器,或分別存儲有效負載長度。

請注意,在.NET 4.5中,<gcAllowVeryLargeObjects>支持可以讓這些值具有適當大小的陣列(例如FixedString[]) - 但請注意,您不希望經常複製數據。爲了避免這種情況,你會希望始終允許備用空間中數組中(這樣你就不會在整個陣列複製只是增加一個項目),並通過與ref單個項目的工作,即

FixedString[] data = ... 
int index = ... 
ProcessItem(ref data[index]); 

void ProcessItem(ref FixedString item) { 
    // ... 
} 

這裏item正在與數組直接對話 - 我們沒有在任何時間複製數據。

現在我們只有一個對象 - 數組本身。

+1

非常感謝,Marc!剛剛開始工作。 「不安全」和「固定」關鍵字有所不同。我會做出答案並更新代碼。我想我也發現了我的思想錯誤:我在原始示例中使用的註釋僅用於編組,並不指導.net如何在內存中組織結構。 –

2
const int BufferSize = 1000000; 

您的緩衝區太大了,因此能夠存儲字符串引用時間過長,並允許它們超過第1代。試驗緩衝區大小提供了這種解決方案:

const int BufferSize = 180000; 

沒有更多的GC(2)集合。

你可以從中推斷出gen#1堆大小。雖然這個測試程序很難做到,但字符串的大小太多了。無論如何,真正的應用程序都需要手動調節。

+0

該應用程序要求日誌足夠大,並且條目足夠長,以使它們進入第二代。這就是我想要模擬的內容。謝謝! –

1

雖然我喜歡馬克Gravell和Hans帕桑特答案(一如既往)...

您可以微調GC爲了同時運行,從而避免凍結時間。 閱讀全文here

+0

我認爲工作站+併發是工作站操作系統上的默認模式,它會導致我的一些線程凍結幾百毫秒。謝謝! –

0

使用StringBuilder的緩衝區基本上與unsafe fixed char[]方法完全相同。但給你一個特定字符串長度的潛在靈活性,超出了你最初分配的範圍(當然,是的,這會導致一個字符串,或者更準確地說StringBuilder的底層char[]有資格進行垃圾回收,但讓我們實用)。另外,你不必自己做字符串長度管理。

private static void TestPerformance2() 
{ 
    var buffer = new StringBuilder[BufferSize]; 
    // Initialize each item of the array. This is no different than what 
    // unsafe struct is. 
    for (int i = 0; i < BufferSize; i++) 
    { 
     buffer[i] = new StringBuilder(256); 
    } 

    GC.Collect(2); 
    Stopwatch stopWatch = new Stopwatch(); 
    var initialCollectionCounts = new int[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) }; 
    stopWatch.Reset(); 
    stopWatch.Start(); 
    for (int i = 0; i < LoopCount; i++) 
    { 
     buffer[i % BufferSize].Clear(); // Or use .Length = 0;, which is what the Clear() method does internally. 

     buffer[i % BufferSize].AppendFormat("{0}", i); 
    } 
    stopWatch.Stop(); 
    Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}", 
     typeof(StringBuilder).Name.PadRight(20), 
     stopWatch.ElapsedMilliseconds, 
     (GC.CollectionCount(0) - initialCollectionCounts[0]), 
     (GC.CollectionCount(1) - initialCollectionCounts[1]), 
     (GC.CollectionCount(2) - initialCollectionCounts[2]) 
    ); 
} 

和使用效果,快兩倍(你甚至可以移動秒錶最多包括數組初始化,它仍然快於UnsafeFixedString)。

Type     Time GC(0) GC(1) GC(2) 

String     4647 131  108  23 
StringBuilder   2600 94  0  0 
UnsafeFixedString  5135 161  0  0