C# 有没有办法减少大数组中新字符串的分配?

Jay*_*Jay 6 c# memory memory-management

我试图了解内存分配以及如何减少它们。

我创建了以下长列表进行测试

var list = new List<int>(Enumerable.Range(0, 1_000_000).ToArray());
Run Code Online (Sandbox Code Playgroud)

然后我循环它并打印一个像这样的字符串

for (var i = 0; i < list.Count; i++) 
{
    Console.WriteLine("Item # " + list[i]); 
}
Run Code Online (Sandbox Code Playgroud)

上一个循环生成了超过 200 万次分配。我相信该行"Item # " + list[i]产生了 200 万个分配。

**问题1**为什么在这种情况下会分配200万个字符串?是否list[i]必须作为字符串存储在堆中,这是 1 次分配,然后是另一个分配中的组合字符串,因此每个循环 2 次分配?

下一步我考虑使用字符串生成器来减少分配

var builder = new StringBuilder();
for (var i = 0; i < list.Count; i++)
{
    builder.Append("Item # ");
    builder.Append(list[i]);

    Console.WriteLine(builder.ToString());
 
    builder.Clear();
}
Run Code Online (Sandbox Code Playgroud)

上面的代码有 100 万次分配,是之前分配的一半。

但是,仍然有很多分配。

我知道字符串是 C# 中的不可变对象,每个字符串在堆中都有自己的分配。但是,有没有一种方法可以重用内存分配,以便我们可以创建 1 个字符串,然后在该循​​环内一遍又一遍地重用相同的分配?就我而言,一旦打印了字符串,我就不再需要它了。对我来说,重用相同的分配并更新它的值是安全的。

**问题2** 是否可以重复使用内存分配来减少分配量?

** 问题 3** 我还可以尝试哪些其他技巧来改进我的循环?

Swe*_*per 9

是否可以重复使用内存分配来减少分配量?

是的。将前缀“Item #”放入单独的变量中,并创建 aSpan<char>来存储 的字符串表示形式list[i]Span每次获得新号码时,您都可以重复使用该号码。

var prefix = "Item # ";
char[] arr = new char[6];
Span<char> number = new(arr);
for (int i = 0 ; i < list.Count ; i++) {
    Console.Write(prefix);
    list[i].TryFormat(number, out int charsWritten);
    Console.Out.WriteLine(number[0..charsWritten]);
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 我们从Span一个char[]. 请注意,数组应该足够大以存储“999999”,即要打印的最长数字(就字符串表示形式而言)。
  • TryFormat写入跨度并告诉我们它写入了多少个字符。我们利用这些信息,以便我们只使用WriteLine它写入的字符,而不再使用。
  • TryFormat返回一个bool指示整数是否成功写入跨度的值。如果这是您担心的事情,请考虑检查一下。
  • 我们必须得到Console.Out才能使用WriteLine需要跨度的重载。Console本身没有这样的静态方法。


Gur*_*ron 5

StringBuilder您可以通过使用与内部类似的技巧(它使用Span's 和ISpanFormattable接口来减少分配)并使用流来完全删除打印循环的分配Console.Out

// allocate big enough buffer to hold the largest formatted number:
char[] buffer = new char[8]; 
var span = buffer.AsSpan();

var allocatedBefore = GC.GetTotalAllocatedBytes(); // to check allocations

for (var i = 0; i < list.Count; i++) 
{
    // write prefix, due to interning it won't be allocated every time
    // in other cases can be moved to outside scope as variable/parameter
    Console.Write("Item # "); 

    // Note - returns bool, in general case might need to check
    // can be false if the span is not big enough
    list[i].TryFormat(span, out int written); 

    // write formatted line to stdout via bufffer
    Console.Out.WriteLine(buffer, 0, written); 
}

// allocations checks
var allocatedAfter = GC.GetTotalAllocatedBytes();
Console.WriteLine(allocatedAfter - allocatedBefore); 
Run Code Online (Sandbox Code Playgroud)

阅读更多: