C#7.2——編寫安全高效的C#代碼

来源:https://www.cnblogs.com/ms27946/archive/2018/11/07/Write_Safe_And_Efficient_Code.html
-Advertisement-
Play Games

原文地址:https://docs.microsoft.com/zh cn/dotnet/csharp/write safe efficient code?view=netcore 2.1 值類型的優勢能避免堆分配。而劣勢就是往往伴隨的數據的拷貝。這就導致了在大量的值類型數據很難的最大化優化這些演算法 ...


原文地址:https://docs.microsoft.com/zh-cn/dotnet/csharp/write-safe-efficient-code?view=netcore-2.1
值類型的優勢能避免堆分配。而劣勢就是往往伴隨的數據的拷貝。這就導致了在大量的值類型數據很難的最大化優化這些演算法操作(因為伴隨著大量數據的拷貝)。而在C#7.2 中就提供了一種機制,它通過對值類型的引用來使代碼更加安全高效。使用這個特性能夠最大化的減小記憶體分配和數據複製操作。

這個新特性主要是以下幾個方面:

  1. 聲明一個 readonly struct 來表示這個類型是不變的,能讓編譯器當它做參數輸入時,會保存它的拷貝。
  2. 使用 ref readonly 。當返回一個值類型,且大於 IntPtr.Size 時以及存儲的生命周期要大於這方法返回的值的時候。
  3. 當用 readonly struct 修飾的變數/類大小大於 IntPtr.Size ,那麼就應該作為參數輸入來傳遞它來提高性能。
  4. 除非用 readonly 修飾符來聲明,永遠不要傳遞一個 struct 作為一個輸入參數(in parameter),因為它可能會產生副作用,從而導致它的行為變得模糊。
  5. 使用 ref struct 或者 readonly ref struct,例如 SpanReadOnlySpan 以位元組流的形式來處理記憶體。

這些技術你要面對權衡這值類型和引用類型這兩個方面帶來的影響。引用類型的變數會分配記憶體到堆記憶體上。值類型變數只包含值。他們兩個對於管理資源記憶體來說都是重要的。值類型當傳遞到一個方法或是從方法中返回時都會拷貝數據。這個行為還包括拷貝值類型成員時,該值的值( This behavior includes copying the value of this when calling members of a value type. )。這個開銷視這個值類型對象數據的大小而定。引用類型是分配在堆記憶體的,每一個新的對象都會重新分配記憶體到堆上。這兩個(值類型和引用)操作都會花費時間。

readonly struct來申明一個不變的值類型結構

用 readonly 修飾符聲明一個結構體,編譯器會知道你的目的就是建立一個不變的結構體類型。編譯器就會根據兩個規則來執行這個設計決定:

  1. 所有的欄位必須是只讀的 readonly。
  2. 所有的屬性必須是只讀的 readonly,包括自動實現屬性。

以上兩條足已確保沒有readonly struct 修飾符的成員來修改結構的狀態—— struct 是不變的

readonly public struct ReadonlyPoint3D {
    public ReadonlyPoint3D (double x, double y, double z) {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public double X { get; }
    public double Y { get; }
    public double Z { get; }
}

儘可能面對大對象結構體使用 ref readonly struct 語句

當這個值不是這個返回方法的本地值時,可以通過引用返回值。通過引用返回的意思是說只拷貝了它的引用,而不是整個結構。下麵的例子中 Origin 屬性不能使用 ref 返回,因為這個值是正在返回的本地變數:

public ReadonlyPoint3D Origin => new ReadonlyPoint3D(0,0,0);

然而,下麵這個例子的屬性就能按引用返回,因為返回的值一個靜態成員:

private static ReadonlyPoint3D origin = new ReadonlyPoint3D(0,0,0);
//註意:這裡返回是內部存儲的易變的引用
public ref ReadonlyPoint3D Origin => ref origin;

你如果不想調用者修改原始值,你可以通過 readonly ref 來修飾返回值:

 public ref readonly ReadonlyPoint3D Origin3 => ref origin;

返回 ref readonly 能夠讓你保存大對象結構的引用以及能夠保護你內部不變的成員數據。

作為調用方,調用者能夠選擇 Origin 屬性是作為一個值還是 按引用只讀的值(ref readonly):

var originValue = Point3D.Origin;
ref readonly var originReference = ref Point3D.Origin;

在上面這段代碼的第一行,把 Point3D 的原始屬性的常數值 Origin 拷貝並複製數據給originValue。第二段代碼只分配了引用。要註意,readonly 修飾符必須是聲明這個變數的一部分。因為這個引用是不允許被修改的。不然,就會引起編譯器編譯錯誤。

readonly 修飾符在申明的 originReference 是必須的。

編譯器要求調用者不能修改引用。企圖直接修改該值會引發編譯器的錯誤。然而,編譯器卻無法知道成員方法修改了結構的狀態。為了確定對象沒有被修改,編譯器會創建一個副本並用它來調用成員信息的引用。任何修改都是對防禦副本(defensive copy)的修改。

對大於 System.IntPtr.Size 的參數應用 in修飾符到 readonly struct

in 關鍵字補充了已經存在的 refout 關鍵字來按引用傳遞參數。in 關鍵字也是按引用傳遞參數,但是調用這個參數的方法不能修改這個值。

值類型作為方法簽名參數傳到調用的方法中,且沒有用下麵的修飾符時,是會發生拷貝操作的。每一個修飾符指定這個變數是按引用傳遞的,避免了拷貝。以及每個修飾符都表達不同的意圖:

  • out:這個方法設置參數的值來作為參數。
  • ref:這個方法也可以設置參數的值來作為參數。
  • in:這個方法作為參數無法修改這個參數的值。

增加 in 修飾符按引用傳遞參數以及申明通過按引用傳值來避免數據的拷貝的意圖。說明你不打算修改這個作為參數的對象。

對於只讀的那些大小超過 IntPtr.Size 的值類型來說,這個經驗經常能提高性能。例如有這些值類型(sbyte,byte,short,ushort,int,uint,long,ulong,char,float,double,decimal 以及 bool 和 enum),任何潛在的性能收益都是很小的。實際上,如果對於小於 IntPtr.Size 的類型使用按引用個傳遞,性能可能會下降。

下麵這段 demo 展示了計算兩個點的3D空間的距離

public static double CalculateDistance ( in Point3D point1, in Point3D point2) {
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;

    return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}

這個方法有兩個參數結構體,每個都有三個 double 欄位。1個 double 8 個位元組,所以每個參數含有 24 位元組。通過指定 in 修飾符,你傳遞了 4 個位元組或 8 個位元組的參數引用,4 還是 8位元組取決平臺結構(32位 一個引用 2 位元組,64位一個引用 4位元組)。這看似大小差異很小,但是當你的應用程式在高併發,高迴圈的情況下調用這個函數,那麼性能上的差距就很明顯了。

in 修飾符也很好的補充了 outref 其他方面。你不能創建僅修飾符(in,out,ref)不同的方法重載。這個新的特性拓展了已經存在 outref 參數原來相同的行為。像 refout 修飾符,值類型由於應用了 in 修飾符而無法裝箱。

in 修飾符能應用在任何成員信息上:方法,委托,lambda表達式,本地函數,索引,操作符。

in 修飾符還有在其他方面的特性,在參數上用 in 修飾的參數值你能使用字面量的值或者常數。不像 refout 參數,你不必在調用方用 in。下麵這段代碼展示了兩個調用 CalculateDistance 的方法。第一個變數使用兩個按引用傳遞的局部變數。第二個包括了作為這個方法調用的一部分創建的臨時變數。

var distance = CalculateDistance (point1,point2);
var fromOrigin = CalculateDistance(point1,new Point3D());

這裡有一些方法,編譯器會強制執行 read-only 簽名的 in 參數。第一個,被調用的方法不能直接分配一個 in 參數。它不能分配到任何 in 欄位,當這個值是值類型的時候。另外,你也不能通過 ref 和 out 修飾符來傳遞一個 in 參數到任何方法上。這些規則都應用在 in 修飾符的參數,前提是提供一個值類型的欄位以及這個參數也是值類型的。事實上,這些規則適用於多個成員訪問,前提是所有級別的成員訪問的類型都是結構體。編譯器強制執行在參數中傳遞的 struct 類型,當它們的 struct 成員用作其他方法的參數時,它們是只讀變數。

使用 in 參數能避免潛在拷貝方面的性能開銷。它不會改變任何方法調用的語義。因此,你無需在調用方(call site)指定 in 修飾符。在調用站省略 in 修飾符會讓編譯器進行參數拷貝操作,有以下幾種原因:

  • 存在隱式轉換,但不存在從參數類型到參數類型的標識轉換。
  • 參數是一個表達式,但是沒有已知的存儲變數。
  • 存在一個不同於已經存在或者是不存在 in 的重載。這種情況下,通過值重載會更好匹配。

這些規則當你更新那些已有的並且已經用 read-only 引用參數的代碼非常有用。在調用方法裡面,你可以通過值參數(value paramters)調用任意成員方法。在那些實例中,會拷貝 in 參數。因為編譯器會對 in 參數創建一個臨時的變數,你可以用 in 指定預設參數的值。下麵這段代碼指定了origins(point 0,0)作為預設值作為第二個參數:

private static double CalculateDistance2 ( in Point3D point1, in Point3D point2 = default) {
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;
    return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}

編譯器會通過引用傳遞只讀參數,指定 in 修飾符在調用方法的參數上,就像下麵展示的代碼:

private static void DemoCalculateDistanceForExplicit (Point3D point1, Point3D point2) {
    var distance = CalculateDistance ( in point1, in point2);
    distance = CalculateDistance ( in point1, new Point3D ());
    distance = CalculateDistance (point1, in Point3D.origin);
}

這種行為能夠更容易的接受 in 參數,隨著時間的推移,大型代碼庫中性能會獲得提高。首先就要添加 in 到方法簽名上。然後你可以在調用端添加 in 修飾符以及新建一個 readonly struct 類型來使編譯器避免在更多未知創建防禦拷貝的副本。

in 參數被設計也能使用在引用類型或數字值。然而,在這種情況的性能收益是很小的。

不要使用易變的結構體作為 in 參數

下麵描述的技術主要解釋了怎樣通過返回引用以及傳遞的值引用避免數據拷貝。當參數類型是已經申明的 readonly struct 類型時,這些技術都能很好的工作。否則,編譯器在很多非只讀參數的場景下必須新建一個防禦拷貝(defensive copies)副本。考慮下麵這段代碼,他計算 3D 點到原地=點的距離:

private static double CalculateDistance ( in Point3D point1, in Point3D point2 = default) {
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;
    return Math.Sqrt (xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}

Point3D 是非只讀結構類型(readonly-ness struct)。在這個方法體中,有 6 個不同的屬性訪問調用。第一次檢查時,你可能覺得這些訪問都是安全的。在這之後,一個 get 讀取器不能修改這個對象的狀態。但是這裡沒有語義規則讓編譯器這樣做。它只是一個通用的約束。任何類型都能實現 get 讀取器來修改這個內部狀態。沒有這些語言保證,在調用任何成員之前,編譯器必須新建這個參數的拷貝副本來作為臨時變數。這個臨時變數存儲在棧上,這個參數的值的副本在這個臨時變數中存儲,並且每個成員訪問的值都會拷貝到棧上,作為參數。在很多情況下,當參數類型不是 readonly struct 時,這些拷貝都會對性能有害,以至於通過值傳遞要比通過只讀引用(readonly reference)傳遞快。

相反,如果距離計算方法使用不變結構,ReadonlyPoint3D,就不需要臨時變數:

private static double CalculateDistance3(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2 = default)
{
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;

    return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}

當你用 readonly struct 修飾的成員時,編譯器會自動生成更多高效代碼:this 引用,而不是接受者的副本拷貝,in 參數總是按引用傳遞到成員方法中。當你使用 readonly struct 作為 in 參數時,這種優化會節省記憶體。

你可以查看程式的demo,在實例代碼倉庫 samples repository 中,它展示了使用 Benchmark.net 比較性能的差異。它比較了傳遞易變結構的值和引用,易變結構的按值傳遞和按引用傳遞。使用不變結構體的按引用傳遞是最快的。

使用 ref struct 類型在單個堆棧幀上處理塊和記憶體

一個語言相關的特性是申明值類型的能力,該值類型必須約束在單個堆棧對上。這個限制能讓編譯器做一些優化。主要推動這個特性體檢在 Span<T>以及相關的結構。你從使用這些新添加的以及更新的.NET API,如 Span<T> 類型來完成性能的提升。

你可能有相同的要求,在記憶體中使用 stackalloc 或者當使用來自於記憶體的交互操作API。你就為這些需求能定義你自己的 ref struct 類型。

readonly ref struct 類型

聲明一個 readonly ref 結構體,它聯合了 ref structreadonly struct 兩者的收益。通過只讀的元素記憶體被限制在單個的棧中,並且只讀元素記憶體無法被修改。

總結

使用值類型能最小化的記憶體分配:

  • 在局部變數和方法參數中值類型存儲在棧上分配
  • 對象的值類型成員做為這個對象的一部分分配在棧上,並不是一個單獨的分配操作。
  • 存儲返回的值類型是在棧上分配

不同於引用類型在相同場景下:

  • 存儲局部變數和方法參數的引用類型分配在堆上,。引用存在棧。
  • 存儲對象的成員變數是引用類型,它作為這個對象的一部分在堆上分配記憶體。而不是單獨的分配這個引用。
  • 存儲返回的值是引用類型,堆分配記憶體。存儲引用的值存儲在棧上。

最小化的記憶體分配要權衡。當結構體記憶體大小超過引用大小時,就要拷貝更多的記憶體。一個引用類型指定 64 位元組或者是 32 位元組,它取決於平臺架構。

這些權衡/折中通常對性能影響很小。然而大對象結構體或大對象集合,對性能影響是遞增的。特別在迴圈和經常調用的地方影響特別明顯。

這些C#語言的增強是為了關鍵演算法的性能而設計的,記憶體分配問題成為了主要的優化點。你會發現你無需經常使用這些特性在你寫的代碼中。然而,這些增強在 .NET 中接受。越來越多的 API 會運用到這些特性,你將看到你的應用程式性能的提升。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2018 11 07日記 概覽 今日立冬, 信息時代帶來的焦躁讓學習無法深入, 所以打算以寫日記的形式戒掉焦躁, 重拾醉心學習的狀態. Synchronized與SyncRoot技術同步線程數據 Serializable特性作用 RPC(遠程方法調用) 數據同步 在多個線程中共用數據, 很容易出現 ...
  • osgi.net是一個動態的模塊化框架。它向用戶提供了模塊化與插件化、面向服務構架和模塊擴展支持等功能。該平臺是OSGi聯盟定義的服務平臺規範移植到.NET的實現。 簡介 尤埃開放服務平臺是一個基於.NET平臺的動態的模塊化中間件,它主要向用戶提供了模塊化與插件化、面向服務架構和模塊擴展三大功能,適 ...
  • 作者:依樂祝 原文地址:https://www.cnblogs.com/yilezhu/p/9926078.html 在本文中,我將帶著大家一步一步的通過圖文的形式來演示如何在Visual Studio Code中進行.NET Core程式的開發,測試以及調試。儘管Visual Studio Cod ...
  • 操作步驟如下: 1、開始 2、運行 3、cmd 4、cd C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319 5、aspnet_regiis.exe -i ...
  • 最近用C#寫qq活動輔助類程式,碰到了會員簽到的gtk演算法不一樣,後來網上找了看,發現有php版的(https://www.oschina.net/code/snippet_1378052_48831) 後來參考了php版的查php相關的資料用C#寫了一個: /// <summary> /// 計算 ...
  • 有朋友發了個Excel.xlsx文件給我,讓我幫忙看看裡面是怎麼做出來的。打開審閱後發現,每個Excel工作表都添加了密碼保護: 看不到裡面的隱藏列和公式等等,感覺很神秘。於是研究了一下Excel文件的格式,做了一個解除工作表密碼的小程式: 原理很簡單:. xlsx文件其實是一個zip壓縮文件,而每 ...
  • 大漠插件就不過多介紹了,不知道的請查下百度。主要是講解C#怎麼調用大漠插件。 大漠插件提供了COM版本,C#直接點擊引用,添加即可。然後註冊下大漠插件到系統文件夾,註冊代碼如下: 調用方法: 實例化大漠模塊: 查找微信和QQ的聊天視窗,根據視窗類名來進行模糊查找,然後得到視窗句柄,根據視窗句柄得到窗 ...
  • 地址:https://github.com/davidfowl/MultiProtocolAspNetCore.git 在一個Kestrel服務上可以同時處理Tcp,Http,Https等多種協議。 通過實現 ConnectionHandler 處理接入連接,ConnectionContext.Tr ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...