日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > C# >内容正文

C#

C#如何安全、高效地玩转任何种类的内存之Span的秉性特点(二)

發(fā)布時(shí)間:2023/12/4 C# 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C#如何安全、高效地玩转任何种类的内存之Span的秉性特点(二) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

讀完上篇《C#如何安全、高效地玩轉(zhuǎn)任何種類的內(nèi)存之Span的本質(zhì)(一)》,相信大家對(duì)span的本質(zhì)應(yīng)該非常清楚了。含著金鑰匙出生的它,從小就被寄予厚望要成為.NET下編寫高性能應(yīng)用程序的重要積木,而且很多老前輩為了接納它,都紛紛做出了改變,比如String、Int、Array。現(xiàn)在,它長(zhǎng)大了,已經(jīng)成為.NET下發(fā)揮關(guān)鍵作用的新值類型和旗艦成員。

那我們又該如何接納它呢?

一句話,熟悉它的脾氣秉性,讓好鋼用到刀刃上

脾氣秉性 - 特點(diǎn)

Slow vs Fast Span

上篇博客介紹了span的本質(zhì),主要涉及到三個(gè)字段,如下:

public struct Span<T> {internal IntPtr _byteOffset; // 偏移量internal object _reference;// 引用,可以看作當(dāng)前對(duì)象的索引internal int _length;// 長(zhǎng)度 }

當(dāng)我們?cè)L問span表示的整體或部分內(nèi)存時(shí),內(nèi)部的索引器通過計(jì)算(ref reference + byteOffset) + index * sizeOf(T)來正確直接地返回實(shí)際儲(chǔ)存位置的引用,而不是通過復(fù)制內(nèi)存來返回相對(duì)位置的副本,從而達(dá)到高性能,但是,現(xiàn)在我要告訴你,這種span被叫做slow span,為什么呢?因?yàn)镃#7.2的新特性ref T支持在簽名中直接返回引用(相當(dāng)于直接整合了這個(gè)過程),這樣就無需通過計(jì)算來確定指針開頭及其起始偏移,從而真正擁有和訪問數(shù)組一樣高的效率,如下:

public struct Span<T> {internal ref T _reference;// 引用,本身已整合_byteOffset、_reference兩者。internal int _length;// 長(zhǎng)度 }

這種只包含兩個(gè)字段的span就叫Fast span

在所有的.NET平臺(tái),Slow Span都是可得到的,但是目前只有.NET Core 2.X原生支持Fast span。

為了讓大家更直觀地了解這兩種Span,下面來做兩組基準(zhǔn)測(cè)試

  • 不同運(yùn)行時(shí)下Span進(jìn)行10萬次Get、Set的基準(zhǔn)測(cè)試

    上圖非常清楚了吧,從Mean(均值)指標(biāo)可以看出差異還是比較大的(約60%),net framework時(shí)代追求生產(chǎn)力,而core時(shí)代追求高性能,所以還是早轉(zhuǎn)core吧,并且新版本core還會(huì)進(jìn)一步優(yōu)化span,差距將會(huì)越來越大。

  • Span vs Array的基準(zhǔn)測(cè)試

    不同運(yùn)行時(shí)下,對(duì)Span和Array進(jìn)行10萬次Get、Set操作

    從上圖Mean(均值)指標(biāo)可以得出:

    • slow span,即運(yùn)行時(shí)原生不支持,在性能上,它的Get、Set操作和數(shù)組差異50%左右。

    • fast span,即運(yùn)行時(shí)原生支持,在性能上,它的Get、Set操作和數(shù)組相當(dāng)。

看了上面測(cè)試,可能有的同學(xué)就會(huì)問了用Array就行了,如果總是操作整個(gè)數(shù)組,這是合適的,但如果想操作數(shù)組的一部分?jǐn)?shù)據(jù)呢?按照以前的做法每次復(fù)制一份相對(duì)位置的副本給調(diào)用方,這就非常消耗性能的,那么如何支持對(duì)完整或部分?jǐn)?shù)組的操作保持同樣高的性能呢?答案就是span,沒有之一。span不僅能用于訪問數(shù)組和分離數(shù)組子集,還可引用來自內(nèi)存任意區(qū)域的數(shù)據(jù),比如本機(jī)代碼、棧內(nèi)存、托管內(nèi)存。

基準(zhǔn)測(cè)試示例源碼參考

Stack-Only

分配一塊棧內(nèi)存是非常快速的,也無需手工釋放,它會(huì)隨著當(dāng)前作用域而釋放,比如方法執(zhí)行結(jié)束時(shí),就自動(dòng)釋放了,所以需要快取快用快放。Span雖然支持所有類型的內(nèi)存,但決定安全、高效地操作各種內(nèi)存的下限自然取決于最嚴(yán)苛的內(nèi)存類型,即棧內(nèi)存,好比木桶能裝多少水,取決于最短的那塊木板。此外,上一篇博客的動(dòng)畫非常清晰地演示了span的本質(zhì),每次都是通過整合內(nèi)部指針為新的引用返回,而.NET運(yùn)行時(shí)跟蹤這些內(nèi)部指針的成本非常高昂,所以將span約束為僅存在于棧上,從而隱式地限制了可以存在的內(nèi)部指針數(shù)量。

備注:棧內(nèi)存的容量非常小, ARM、x86 和 x64 計(jì)算機(jī),默認(rèn)堆棧大小為 1 MB。CLR和編譯器會(huì)自動(dòng)檢測(cè)Stack-Only約束。

所以span必須是值類型,它不能被儲(chǔ)存到堆上。

違背Stack-Only的應(yīng)用場(chǎng)景

  • Span不能作為類的字段

    class Impossible {Span<byte> field; }
  • Span不能實(shí)現(xiàn)任何接口

    先來看一段C#(偽代碼):

    struct StructType<T> : IEnumerable<T> { } class SpanStructTypeSample {static void Test(){var value = new StructType<int>();Parse(value);}static void Parse(IEnumerable<int> collection) { } }

    使用ILDasm查看生成的IL代碼:

    .method public hidebysig static void Test() cil managed // 調(diào)用Test方法 {// Code size 22 (0x16).maxstack 1.locals init (valuetype SpanTest.StructType`1<int32> V_0)IL_0000: nopIL_0001: ldloca.s V_0IL_0003: initobj valuetype SpanTest.StructType`1<int32>IL_0009: ldloc.0IL_000a: box valuetype SpanTest.StructType`1<int32> // 裝箱,意味著被儲(chǔ)存到托管堆上。IL_000f: call void SpanTest.SpanStructTypeSample::Parse(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)IL_0014: nopIL_0015: ret } // end of method SpanStructTypeSample::Test

    上面的代碼很明確,首先讓自定義的值類型實(shí)現(xiàn)接口IEnumerable,然后作為參數(shù)傳遞給Parse,最后分析IL代碼發(fā)現(xiàn)參數(shù)被裝箱了,意味著將被儲(chǔ)存到托管堆上,如果將來C#能專門定義只用于struct的接口,那么就能擴(kuò)展Stack-Only結(jié)構(gòu)到此應(yīng)用場(chǎng)景了,一起期待吧。

  • Span不能作為異步方法的參數(shù)

    首先async和await?是非常棒的語法糖,不僅僅大大地簡(jiǎn)化了編寫異步代碼的難度,而且還帶來了代碼的優(yōu)雅度。

    同樣,先來看一段C#代碼:

    public async Task TestAsync(Span<byte> data) { }

    這樣的用法也是禁止的,編譯時(shí)就會(huì)報(bào)錯(cuò)Parameter or local type Span<byte> cannot be declared in async method.。因?yàn)楸举|(zhì)上,async?&?await?的內(nèi)部是通過AsyncMethodBuilder來創(chuàng)建一個(gè)異步的狀態(tài)機(jī),某一時(shí)刻可能會(huì)將方法參數(shù)儲(chǔ)存到托管堆上。

  • Span不能作為泛型類型的參數(shù)

    同樣,先來看一段C#代碼:

    Func<Span<byte>> valueProvider = () => new Span<byte>(new byte[256]); object value = valueProvider.Invoke(); // 裝箱

    這樣的用法也是禁止的,編譯時(shí)會(huì)報(bào)錯(cuò)The type Span<byte>may not be used as a type argument.。同理,span<byte>可以表示內(nèi)存任意區(qū)域,而實(shí)際使用時(shí)肯定需要類型化對(duì)象,無法避免裝箱。那么微軟為什么不引入一種新的泛型約束:stackonly,而是決定禁止span作為泛型參數(shù),因?yàn)檫@需要編譯器檢查所有的代碼,可能還需要理解代碼邏輯(因?yàn)橛械念愋托枰\(yùn)行時(shí)才能確定),不然是無法保證stackonly約束的,呵呵,目前看來是不現(xiàn)實(shí)的,不知人工智能能否解決這個(gè)問題。

  • Stack Tearing

    闡述這個(gè)特點(diǎn)前,先簡(jiǎn)單說說計(jì)算機(jī)的字大小。

    • 計(jì)算機(jī)的字大小

      表示計(jì)算機(jī)中CPU的字長(zhǎng),32位CPU字長(zhǎng)為32位,即4字節(jié);64位CPU字長(zhǎng)為64位,即8字節(jié)。CPU的字長(zhǎng)決定了每次能夠原子更新的連續(xù)內(nèi)存塊的大小

    棧撕裂其實(shí)是多線程下的數(shù)據(jù)同步問題,當(dāng)結(jié)構(gòu)數(shù)據(jù)大于當(dāng)前處理器的字大小時(shí),都會(huì)面臨這個(gè)問題。如前所述,span內(nèi)部包含多個(gè)字段,這就意味著,一些處理器可能無法保證原子更新span的_reference和_length?字段,也就是說,多線程下_reference和_length可能來自于兩個(gè)不同的span。

    internal class Buffer {Span<byte> _memory = new byte[1024];public void Resize(int newSize){_memory = new byte[newSize]; // 因?yàn)檫@里無法保證原子更新}public byte this[int index] => _memory[index]; // 所以這里可能的部分更新 }

    其實(shí)有兩種辦法可以解決這個(gè)問題:

  • 直接處理 - 加鎖,即強(qiáng)制同步訪問。

  • 間接處理 - 私有化字段,即不給外面觀察到部分更新的機(jī)會(huì)。

  • 如果這樣,就無法保證像數(shù)組一樣的高性能,因此不能給字段加鎖,也不能限制訪問(沒意義),另外對(duì)Span的訪問和寫入都是直接操作的內(nèi)存,如果_reference和_length出現(xiàn)不同步的情況,還會(huì)導(dǎo)致內(nèi)存安全問題。

    這也是為什么span只能存在于棧上,即指針、數(shù)據(jù)、長(zhǎng)度全都存于棧上,而不是引用存在棧,數(shù)據(jù)存在堆,因?yàn)?strong>span<T>不需要暫留,必須快取快用快放,否則就不要使用span。

    備注:對(duì)于需要暫留到堆上的場(chǎng)景,它的解決方案是Memory<T>,大家可以繼續(xù)關(guān)注。

    .NET庫(kù)的集成

    為了支持輕松高效地處理 {ReadOnly}Span?,微軟向.NET添加了數(shù)百個(gè)新成員和類型。目前大多是基于數(shù)組、字符串和基元類型的方法的重載 ,除此之外,還包括一些專注于特定處理方面的全新類型,比如:System.IO.Pipelines。

    下面是一些比較常用的擴(kuò)展:

  • 基元類型(偽代碼)

    short.Parse(ReadOnlySpan<char> s); int.Parse(ReadOnlySpan<char> s); long.Parse(ReadOnlySpan<char> s); DateTime.Parse(ReadOnlySpan<char> s); TimeSpan.Parse(ReadOnlySpan<char> input); Guid.Parse(ReadOnlySpan<char> input);
  • 字符串

    public static ReadOnlySpan<char> AsSpan(this string text, int start, int length); public static ReadOnlySpan<char> AsSpan(this string text, int start); public static ReadOnlySpan<char> AsSpan(this string text); public static String Create<TState>(int length, TState state, SpanAction<char, TState> action);
  • 數(shù)組

    public static Span<T> AsSpan<T>(this T[] array, int start); public static Span<T> AsSpan<T>(this T[] array); public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start, int length); public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start); public static Span<T> AsSpan<T>(this T[] array, int start, int length);
  • Guid

    public static bool TryParse(ReadOnlySpan<char> input, out Guid result); public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default (ReadOnlySpan<char>));
  • 最后使用上面的API演示一個(gè)官網(wǎng)的例子,解析字符串"123,456"中的數(shù)字:

    以前的寫法

    var input = "123,456"; var commaPos = input.IndexOf(','); var first = int.Parse(input.Substring(0, commaPos));// yes-Allocating, yes-Coping var second = int.Parse(input.Substring(commaPos + 1));// yes-Allocating, yes-Coping

    現(xiàn)在的寫法

    var input = "123,456"; var inputSpan = input.AsSpan(); var commaPos = input.IndexOf(','); var first = int.Parse(inputSpan.Slice(0, commaPos));// no-Allocating, no-Coping var second = int.Parse(inputSpan.Slice(commaPos + 1));// no-Allocating, no-Coping

    當(dāng)然還是有許多這樣的方法,比如System.Random、System.Net.Socket、Utf8Formatter、Utf8Parser等,明白了它的脾氣秉性,對(duì)于具體的應(yīng)用場(chǎng)景大家可以先自行查閱資料,相信認(rèn)真讀完上篇、本篇的同學(xué)已經(jīng)具備用好這把尖刀的能力了。

    總結(jié)

    綜上所訴,通過限制Span只能駐留到棧上,完美解決了以下的問題:

  • 更高效地內(nèi)存訪問,快取快用快放的天然保障

  • 更高效地GC跟蹤

  • 并發(fā)內(nèi)存安全

  • 備注:正是由于Stack-Only這個(gè)特點(diǎn),在底層數(shù)據(jù)訪問、轉(zhuǎn)換以及同步處理方面,Span性能非常出色。

    此外,本篇還在上篇的基礎(chǔ)上,詳細(xì)講解span的脾氣秉性,以及每種特點(diǎn)下的非法應(yīng)用場(chǎng)景,一切都是為了大家能夠在.NET 程序中使用span高效安全地訪問內(nèi)存,希望大家能有所收獲。下一篇可能會(huì)講span的加強(qiáng),也可能會(huì)講它在數(shù)據(jù)轉(zhuǎn)換以及同步處理方面的應(yīng)用,比如:Data Pipelines、Discontinuous Buffers、Buffer Pooling等,也可能會(huì)講Memory<T>,感興趣請(qǐng)繼續(xù)關(guān)注。

    最后

    如果有什么疑問和見解,歡迎評(píng)論區(qū)交流。
    如果你覺得本篇文章對(duì)您有幫助的話,感謝您的【推薦】。
    如果你對(duì)高性能編程感興趣的話可以關(guān)注我,我會(huì)定期的在博客分享我的學(xué)習(xí)心得。
    歡迎轉(zhuǎn)載,請(qǐng)?jiān)诿黠@位置給出出處及鏈接

    延伸閱讀

    https://adamsitnik.com/Hardware-Counters-Diagnoser/#how-to-get-it-running-for-net-coremono-on-windows

    https://blogs.msdn.microsoft.com/dotnet/2017/10/16/ryujit-just-in-time-compiler-optimization-enhancements

    https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.Fast.cs

    https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cs

    https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code

    總結(jié)

    以上是生活随笔為你收集整理的C#如何安全、高效地玩转任何种类的内存之Span的秉性特点(二)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。