重要
本部分中介绍的技术改进了在代码中应用于 热路径 时的性能。 热路径是指在正常操作中经常执行和重复执行的代码库部分。 将这些技术应用于不经常执行的代码将影响最小。 在进行任何更改以提高性能之前,必须衡量基线。 然后,分析该基线以确定内存瓶颈的发生位置。 可以在诊断 和检测部分中了解许多跨平台工具来衡量应用程序的性能。 可以在教程中练习探查会话,以衡量 Visual Studio 文档中的内存使用情况。
测量内存使用情况并确定可以减少分配后,请使用本部分中的技术来减少分配。 每次连续更改后,再次测量内存使用情况。 确保每次更改都会对应用程序中的内存使用率产生积极影响。
.NET 中的性能工作通常意味着从代码中删除分配。 分配的每个内存块最终都必须释放。 减少分配可减少垃圾回收所用的时间。 此功能会从特定代码路径中移除垃圾回收,从而可更准确地预测执行时间。
减少分配的常见策略是将关键数据结构从 class
类型更改为 struct
类型。 此更改会影响使用这些类型的语义。 参数和返回值现在通过值传递,而不是通过引用传递。 如果类型较小、三个单词或更少(考虑一个单词是一个整数的自然大小),则复制值的成本可忽略不计。 它可被量化,并且可能对较大的数据类型产生实际的性能影响。 为了对抗复制的效果,开发人员可以通过 ref
传递这些类型,以恢复它们的预期语义。
C# 的ref
功能使你能够为struct
类型表达所需的语义,而不会对其整体可用性产生负面影响。 在这些增强功能之前,开发人员需要利用 unsafe
指针和原始内存构造来实现相同的性能影响。 编译器为新的相关功能生成ref
。
可验证的安全代码 意味着编译器检测到可能的缓冲区溢出或访问未分配或释放的内存。 编译器检测并阻止某些错误。
通过引用传递和返回
C# 中的变量存储 值。 在类型中 struct
,该值是该类型的实例的内容。 在类型中 class
,该值是对存储该类型的实例的内存块的引用。
ref
添加修饰符意味着变量存储对值的引用。 在 struct
类型中,引用指向包含值的存储。 在 class
类型中,引用指向包含内存块引用的存储。
在 C# 中,方法的参数 按值传递,返回值 按值返回。 参数 的值 传递给方法。 返回参数 的值 是返回值。
ref
、in
、ref readonly
或out
修饰符指示参数是通过引用传递的。 对存储位置的 引用 将传递给该方法。 将 ref
添加到方法签名中表示返回值是 通过引用返回。 对存储位置的 引用 是返回值。
还可以使用 ref 赋值 让变量引用另一个变量。 典型的赋值将右侧 的值 复制到赋值左侧的变量。
ref 赋值将右侧变量的内存位置复制到左侧的变量。 现在 ref
引用原始变量:
int anInteger = 42; // assignment.
ref int location = ref anInteger; // ref assignment.
ref int sameLocation = ref location; // ref assignment
Console.WriteLine(location); // output: 42
sameLocation = 19; // assignment
Console.WriteLine(anInteger); // output: 19
分配变量时,可以更改其值。 使用 ref 分配变量时,会更改变量引用的内容。
可以利用 ref
变量、按引用传递和 ref 分配来直接使用值存储。 直接使用存储时,编译器强制实施的范围规则可确保安全。
这ref readonly
in
两个修饰符都表示参数应通过引用传递,并且无法在方法中重新分配。 区别在于, ref readonly
指示该方法使用参数作为变量。 该方法可以捕获参数,也可以通过只读引用返回参数。 在这些情况下,应使用 ref readonly
修饰符。 否则, in
修饰符可提供更大的灵活性。 不需要将 in
修饰符添加到 in
参数的参数中,因此可以安全地使用 in
修饰符来更新现有的 API 签名。 如果您没有在ref
参数的参数中添加in
或ref readonly
修饰符,编译器将发出警告。
引用安全上下文
C# 包含有关 ref
表达式的规则,确保在存储失效后,ref
表达式无法被访问。 请看下面的示例:
public ref int CantEscape()
{
int index = 42;
return ref index; // Error: index's ref safe context is the body of CantEscape
}
编译器报告错误,因为无法从方法返回对局部变量的引用。 调用方无法访问所引用的存储。
ref 安全上下文定义ref
表达式安全访问或修改的范围。 下表列出了变量类型的 ref 安全上下文 。
ref
字段不能在 class
中声明,也不能在非 ref struct
中声明,因此这些行不在表中。
声明 | ref 安全上下文 |
---|---|
非引用本地 | 在其中声明本地的块 |
非引用参数 | 当前方法 |
ref 、ref readonly 、in 参数 |
调用方法 |
out 参数 |
当前方法 |
class 字段 |
调用方法 |
非引用 struct 字段 |
当前方法 |
ref 的 ref struct 字段 |
调用方法 |
如果ref
是调用方法,则可以返回变量。 如果 ref 安全上下文 是当前的方法或代码块,那么 ref
返回是被禁止的。 以下代码片段显示了两个示例。 可以从调用方法的范围访问成员字段,因此类或结构字段的 ref 安全上下文 是调用方法。 含 或 ref
修饰符的参数的in
是整个方法。 两者都可以通过成员方法返回 ref
:
private int anIndex;
public ref int RetrieveIndexRef()
{
return ref anIndex;
}
public ref int RefMin(ref int left, ref int right)
{
if (left < right)
return ref left;
else
return ref right;
}
注释
当ref readonly
或in
修饰符应用于某个参数时,该参数可以由ref readonly
返回,而不是由ref
返回。
编译器确保引用不能逃逸其ref 安全上下文。 可以安全地使用 ref
参数、ref return
和 ref
局部变量,因为编译器会检测你的代码是否意外包含了在存储无效时可访问的 ref
表达式。
安全上下文和引用结构
ref struct
类型需要更多规则来确保可以安全地使用它们。 类型 ref struct
可以包含 ref
字段。 这需要引入 安全上下文。 对于大多数类型, 安全上下文 是调用方法。 换句话说,始终可以从方法返回一个非ref struct
的值。
通俗而言, 的 ref struct
是可以在其中访问其所有 ref
字段的范围。 换言之,它是其所有 字段的ref
的交集。 以下方法返回 ReadOnlySpan<char>
到一个成员字段,因此该方法的 安全上下文 是:
private string longMessage = "This is a long message";
public ReadOnlySpan<char> Safe()
{
var span = longMessage.AsSpan();
return span;
}
相反,以下代码会产生错误,因为 ref field
的 Span<int>
成员引用的是堆栈分配的整数数组。 它无法避开这个方法。
public Span<int> M()
{
int length = 3;
Span<int> numbers = stackalloc int[length];
for (var i = 0; i < length; i++)
{
numbers[i] = i;
}
return numbers; // Error! numbers can't escape this method.
}
统一内存类型
System.Span<T> 和 System.Memory<T> 的引入提供了用于处理内存的统一模型。
System.ReadOnlySpan<T> 和 System.ReadOnlyMemory<T> 提供用于访问内存的只读版本。 它们都提供对存储类似元素数组的内存块的抽象。 区别在于,Span<T>
和ReadOnlySpan<T>
是ref struct
类型,而Memory<T>
和ReadOnlyMemory<T>
是struct
类型。 跨度包含 ref field
。 因此,跨度的实例不能退出其安全上下文。
的ref struct
是其 的ref field
。 实施 Memory<T>
并 ReadOnlyMemory<T>
删除此限制。 使用这些类型直接访问内存缓冲区。
利用 ref 安全性提高性能
使用这些功能来提高性能涉及以下任务:
-
避免分配:当您将类型从
class
更改为struct
时,可以更改其存储方式。 局部变量存储在堆栈上。 分配容器对象时,成员将内联存储。 此更改意味着分配更少,并且会减少垃圾回收器所做的工作。 它还可能会降低内存压力,以便垃圾回收器运行频率较低。 -
保留引用语义:将类型从
class
更改为struct
会改变变量传递给方法时的语义。 修改其参数状态的代码需要修改。 现在,参数是一个struct
,该方法正在修改原始对象的副本。 可以通过将ref
作为参数传递来还原原始语义。 更改后,该方法将再次修改原始struct
方法。 -
避免复制数据:复制较大的
struct
类型可能会影响某些代码路径的性能。 还可以添加ref
修饰符,以便通过引用而不是值将较大的数据结构传递给方法。 -
限制修改:通过引用传递类型时
struct
,调用的方法可以修改结构的状态。 可以将ref
修饰符替换为ref readonly
或in
修饰符,以指示参数不可修改。 当方法捕获参数或通过只读引用返回参数时首选ref readonly
。 还可以创建包含readonly struct
成员的struct
类型或readonly
类型,以便更好地控制struct
中哪些成员可以被修改。 -
直接操作内存:某些算法在将数据结构视为包含一系列元素的内存块时效率最高。 这些
Span
和Memory
类型提供对内存块的安全访问。
这些技术都不需要 unsafe
代码。 明智地使用,可以从以前只能使用不安全技术的安全代码中获取性能特征。 可以在教程中自行尝试 减少内存分配的技术。