如何避免长生存字符串导致第 2 代垃圾回收
本文关键字:何避免 字符串 | 更新日期: 2023-09-27 18:32:28
我有一个应用程序,我将日志字符串保存在循环缓冲区中。当日志填满时,对于每个新插入,旧字符串将被释放以进行垃圾回收,然后它们位于第 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<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])
);
}
}
}
编辑:使用不安全固定字符串更新了代码,该代码执行所需的工作:
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
具有string
字段的struct
在这里有所不同也就不足为奇了:string
字段始终只是对托管堆上对象的引用 - 特别是某处的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
直接与数组中的元素对话 - 我们没有在任何时候复制数据。
现在我们只有一个对象 - 数组本身。
const int BufferSize = 1000000;
您的缓冲区太大了,因此能够存储字符串引用太长时间并允许它们提升到 gen#1 以上。 试验缓冲区大小可提供以下解决方案:
const int BufferSize = 180000;
不再有 GC(2( 集合。
您可以从中推断出 gen#1 堆大小。 尽管对于此测试程序很难做到,但字符串大小变化太大。 无论如何,在真正的应用程序中都需要手动调整。
虽然我喜欢马克·格拉维尔和汉斯·帕桑特的答案(一如既往(......
您可以微调 GC 以便并发运行,从而避免冻结时间。在此处阅读有关它的信息
使用 StringBuilder
s 的缓冲区本质上与 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