《深入理解C#》书里涉及到的内容与知识点
Span 和 stackalloc
.NET 提供了一些方式来访问内存块。最常用的就是数组,ArraySegment
int ReadData(byte[] buffer, int offset, int length)
其中的 “buffer,offset,length” 这些参数在 .NET 中到处可见,这实际上是一种征兆,表明我们没有正确的抽象,可以继续优化的。而 Span<T>
就是为了修复这个问题的。
Span
Span
- 它允许 span 引用具有严格控制生命周期的内存,因为 span 只能存在于堆栈中。分配内存的代码能传递一个 span 到其它代码,并释放内存,随后确保这里没有剩下任何 span 引用到了释放的内存。
- 它允许在 span 中自定义一次性初始化数据,不会发生拷贝,也不存在代码事后更改数据的风险。
通过下面生成随机字符串的例子来看一下。首先是传统代码:
public static string Generate(string alphabet, Random random, int length)
{
char[] chars = new char[length];
for (int i = 0; i < length; i++)
{
chars[i] = alphabet[random.Next(alphabet.Length)];
}
return new string(chars);
}
传统方法这里有两初进行了内存分配:一个是 chars 数组,一个是 return new string(),数据还发生了拷贝:数据需要从 chars 复制到新的对象以便构造 string。
我们可以使用不安全代码来优化:
unsafe static string Generate2(string alphabet, Random random, int length)
{
char* chars = stackalloc char[length];
for (int i = 0; i < length; i++)
{
chars[i] = alphabet[random.Next(alphabet.Length)];
}
return new string(chars);
}
上面的代码只发生了一次堆分配:就是 string()。临时缓冲区是分配给堆栈的,但是您需要使用 unsafe 修饰符,因为您使用的是指针。尽量不要使用不安全代码,很容易出现问题,尽管能保证上面的代码是没问题的。
一个好消息就是我们可以通过 Span
public static string Generate3(string alphabet, Random random, int length)
{
Span<char> chars = stackalloc char[length];
for (int i = 0; i < length; i++)
{
chars[i] = alphabet[random.Next(alphabet.Length)];
}
return new string(chars);
}
通过 Span + stackalloc 安全代码来实现不安全代码同样的效果。但是这样仅仅只是少了一次内存分配,但数据复制还是没有避免。我们可以继续优化:
public static string Generate4(string alphabet, Random random, int length) =>
string.Create(length, (alphabet, random), (span, state) =>
{
var alphabet2 = state.alphabet;
var random2 = state.random;
for (int i = 0; i < span.Length; i++)
{
span[i] = alphabet2[random2.Next(alphabet2.Length)];
}
});
通过 span 以及内置的 public static String Create<TState>(int length, TState state, SpanAction<char, TState> action);
来减少数据的拷贝。