使用新的 C# 功能減少記憶體配置

重要

本節所述的技術可改善套用至程式碼最忙碌路徑時的效能。 最忙碌路徑是您程式碼基底的區段,這些區段會在一般作業中經常性地重複執行。 將這些技術套用至未經常執行的程式碼可將影響降到最低。 請務必先測量基準,再進行變更以改善效能。 接著再分析該基準,以判斷記憶體瓶頸發生的位置。 您可以在診斷和檢測一節了解許多跨平台工具來測量應用程式的效能。 您可以在 Visual Studio 文件中練習分析工作階段,藉此測量記憶體使用量

當您測量記憶體使用量並判斷可減少配置之後,請使用本節中的技術來減少配置。 每次連續變更之後,請再次測量記憶體使用量。 請確定每個變更對應用程式的記憶體使用量有正面影響。

.NET 中的效能工作通常表示從程式碼移除配置。 您配置的每個記憶體區塊最終都必須釋放。 較少的配置可減少記憶體回收所花費的時間。 其允許從特定程式碼路徑移除記憶體回收,以取得更多可預測的執行時間。

減少配置的常見策略是將重要的資料結構從 class 型別變更為 struct 型別。 這項變更會影響使用這些型別的語意。 參數和傳回資料現在會以值進行傳遞,而不是藉傳址來傳遞。 如果型別很小、僅有或小於三個字,則可忽略值的複製成本 (考慮到一個字組是一個整數的自然大小)。 這是可測量的,且對較大型別而言可能會有實際的效能影響。 為了克服複製程序的影響,開發人員可以透過 ref 來傳遞這些型別,以傳回預期的語意。

C# ref 功能可讓您表達 struct 型別所需的語意,而不會對整體可用性造成負面影響。 在這些增強功能之前,開發人員必須利用指標和原始記憶體來回復為 unsafe 建構,以達到相同的效能影響。 編譯器會針對新的 ref 相關功能產生可驗證的受控碼可驗證的受控碼表示編譯器會偵測可能的緩衝區溢位,或者存取未配置或釋放的記憶體。 編譯器會偵測並防止某些錯誤。

以傳址方式傳遞和傳回

C# 的變數會儲存。 在 struct 型別中,值是型別執行個體的內容。 在 class 型別中,值是儲存型別執行個體的記憶體區塊參考。 新增 ref 修飾元表示變數儲存在值的參考中。 在 struct 型別中,參考會指向包含值的儲存體。 在 class 型別中,參考會指向包含記憶體區塊參考的儲存體。

在 C# 中,方法的參數會以值傳遞,而傳回值則是以值傳回。 引數的會傳遞至方法。 傳回引數的是傳回值。

refinref readonlyout 修飾元表示引數會藉傳址傳遞。 儲存體位置的參考會傳遞至方法。 新增 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 readonlyin 修飾元都表示引數應該以傳址方式傳遞,而且無法在方法中重新指派。 差異在於 ref readonly 表示方法會使用參數做為變數。 方法可能會擷取參數,或藉由唯讀參考傳回參數。 在這些情況下,您應該使用 ref readonly 修飾元。 否則,in 修飾元可提供更大的彈性。 您不需要將 in 修飾元新增至 in 參數的引數,因此您可以使用 in 修飾元安全地更新現有的 API 簽章。 如果您未將 refin 修飾元新增至 ref readonly 參數的引數,編譯器會發出警告。

Ref 安全內容

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 參數 current 方法
refref readonlyin 參數 呼叫方法
out 參數 current 方法
class 欄位 呼叫方法
非 ref struct 欄位 current 方法
ref structref 欄位 呼叫方法

如果變數的呼叫方法是 ref 安全內容,變數即可以 ref 方式傳回。 如果其 ref 安全內容是目前的方法或區塊,則不允許傳回 ref。 下列程式碼片段會顯示兩個範例。 由於成員欄位可以透過呼叫方法從範圍中加以存取,因此類別或結構欄位的呼叫方法即是 ref 安全內容。 針對使用 refin 修飾元的參數,其 ref 安全內容即為完整的方法。 這兩者都可以 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 readonlyin 修飾元套用至參數時,該參數即可透過 ref readonly (而非 ref) 傳回。

編譯器可確保參考無法逸出其 ref 安全內容。 您可以安全使用 ref 參數、ref returnref 區域變數,因為編譯器可偵測您是否不小心撰寫了程式碼,其中 ref 運算式可在其儲存體無效時加以存取。

安全內容和 ref 結構

ref struct 型別需要更多規則,以確保可以安全使用。 ref struct 型別可以包含 ref 欄位。 這需要引進安全內容。 對於大部分的型別,呼叫方法為安全內容。 換句話說,不是 ref struct 的值一律可以從方法傳回。

以一般角度而言,ref struct安全內容是可以存取其所有 ref 欄位的範圍。 換句話說,這是其所有 ref 欄位的 ref 安全內容交集。 下列方法會將 ReadOnlySpan<char> 傳回至成員欄位,因此其安全內容是方法:

private string longMessage = "This is a long message";

public ReadOnlySpan<char> Safe()
{
    var span = longMessage.AsSpan();
    return span;
}

相反地,下列程式碼會發出錯誤,因為 Span<int>ref field 成員會參考已配置整數陣列的堆疊。 其無法逸出方法:

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 fieldref 安全內容Memory<T>ReadOnlyMemory<T> 的實作會移除這項限制。 您可以使用這些型別直接存取記憶體緩衝區。

使用 ref 安全性改善效能

使用這些功能改善效能時會涉及以下工作:

  • 避免配置:當您將型別從 class 變更為 struct 時,即代表變更其儲存方式。 區域變數會儲存在堆疊上。 成員會在配置容器物件時以內嵌方式儲存。 這項變更表示配置較少,且會減少記憶體回收行程執行的工作。 其也可能會降低記憶體壓力,讓記憶體回收行程的執行頻率變低。
  • 保留參考語意:將型別從 class 變更為 struct,會變更將變數傳遞至方法的語意。 修改其參數狀態的程式碼需要進行修改。 現在參數是 struct,方法正在修改原始物件的複本。 您可以傳遞該參數做為 ref 參數,以還原原始語意。 在進行該變更之後,方法會再次修改原始 struct
  • 避免複製資料:複製較大的 struct 型別可能會影響某些程式碼路徑的效能。 您也可以新增 ref 修飾元,藉傳址方式將較大的資料結構傳遞至方法,而不是透過值來傳遞。
  • 限制修改:當 struct 型別藉傳址方式傳遞時,呼叫的方法可以修改結構狀態。 您可以將 ref 修飾元以 ref readonlyin 修飾元取代,以表示無法修改該引數。 當方法擷取參數或透過唯讀參考將其傳回時,偏好 ref readonly。 您也可以建立 readonly struct 型別,或具有 readonly 成員的 struct 型別,以進一步控制可修改的 struct 成員。
  • 直接操作記憶體:當將資料結構視為包含元素序列的記憶體區塊時,使用某些演算法會最有效率。 SpanMemory 型別可讓您安全地存取記憶體區塊。

這些技術都不需要 unsafe 程式碼。 靈活運用即可取得受控碼的效能特性,揮別只能使用不安全技術來取得效能特性的過往。 您可以在減少記憶體配置教學課程中自行嘗試這些技術。