如何避免长寿命字符串导致第2代垃圾收集
我有一个应用程序,我将日志字符串保存在循环缓冲区中。 当日志变满时,对于每个新插入,旧的字符串将被释放用于垃圾收集,然后它们在第2代内存中。 因此,最终将发生第2代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(s => s); TestPerformance(s => new FixedString(s)); TestPerformance(s => s); Console.WriteLine(); } Console.ReadKey(); } private static void TestPerformance(Func 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 s); TestPerformance(s => s); Console.WriteLine(); } Console.ReadKey(); } private static void TestPerformance(Func 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
带string
字段的struct
在这里有所不同应该不足为奇: string
字段始终只是对托管堆上对象的引用 – 特别是某个string
对象。 该string
仍然存在,最终仍会导致GC2。
“修复”这个问题的唯一方法就是不要把它作为一个对象; 并且唯一的方法(不完全超出托管内存)是使用fixed
缓冲区:
public unsafe struct FixedString { private fixed char str[100]; }
这里, 每个结构实例FixedString
都有200个字节为数据保留。 str
只是char*
的相对偏移量,表示此预留的开始。 但是,使用它很棘手 – 并且需要unsafe
代码。 另请注意,无论您是否确实要存储3个字符或170,每个FixedString
保留相同的空间量。为避免内存问题,您可能需要使用null-teriminator,或分别存储有效负载长度。
请注意,在.NET 4.5中,
支持可以使这些值具有相当大的数组(例如, FixedString[]
) – 但请注意,您不希望经常复制数据。 为避免这种情况,您可能希望始终允许数组中的备用空间(因此您不要仅仅为了添加一个项目而复制整个数组),并通过ref
处理单个项目,即
FixedString[] data = ... int index = ... ProcessItem(ref data[index]); void ProcessItem(ref FixedString item) { // ... }
这里的item
直接与数组中的元素对话 – 我们没有在任何时候复制数据。
现在我们只有一个对象 – 数组本身。
const int BufferSize = 1000000;
你的缓冲区太大了,因此能够存储一个字符串引用太长时间,并允许它们被提升到第一代。 尝试使用缓冲区大小可提供此解决方案:
const int BufferSize = 180000;
不再有GC(2)集合。
你可以从中推断出gen#1堆大小。 虽然这个测试程序很难做到,但字符串大小变化太大。 无论如何,在真实应用程序中都需要进行手动调整。
虽然我喜欢Marc Gravell和Hans Passant的答案(一如既往)……
您可以微调GC以便同时运行,从而避免冻结时间。 在这里阅读它
使用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