<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      .NET 零開銷抽象指南

      背景

      2008 年前后的 Midori 項目試圖構建一個以 .NET 為用戶態基礎的操作系統,在這個項目中有很多讓 CLR 以及 C# 的類型系統向著適合系統編程的方向改進的探索,雖然項目最終沒有面世,但是積累了很多的成果。近些年由于 .NET 團隊在高性能和零開銷設施上的需要,從 2017 年開始,這些成果逐漸被加入 CLR 和 C# 中,從而能夠讓 .NET 團隊將原先大量的 C++ 基礎庫函數用 C# 重寫,不僅能減少互操作的開銷,還允許 JIT 進行 inline 等優化。

      與常識可能不同,將原先 C++ 的函數重寫成 C# 之后,帶來的結果反而是大幅提升了運行效率。例如 Visual Studio 2019 的 16.5 版本將原先 C++ 實現的查找與替換功能用 C# 重寫之后,更是帶來了超過 10 倍的性能提升,在十萬多個文件中利用正則表達式查找字符串從原來的 4 分多鐘減少只需要 20 多秒。

      目前已經到了 .NET 7 和 C# 11,我們已經能找到大量的相關設施,不過我們仍處在改進進程的中途。

      本文則利用目前為止已有的設施,講講如何在 .NET 中進行零開銷的抽象。

      基礎設施

      首先我們來通過以下的不完全介紹來熟悉一下部分基礎設施。

      refoutinref readonly

      談到 refout,相信大多數人都不會陌生,畢竟這是從 C# 1 開始就存在的東西。這其實就是內存安全的指針,允許我們在內存安全的前提之下,享受到指針的功能:

      void Foo(ref int x)
      {
          x++;
      }
      
      int x = 3;
      ref int y = ref x;
      y = 4;
      Console.WriteLine(x); // 4
      Foo(ref y);
      Console.WriteLine(x); // 5
      

      out 則多用于傳遞函數的結果,非常類似 C/C++ 以及 COM 中返回調用是否成功,而實際數據則通過參數里的指針傳出的方法:

      bool TryGetValue(out int x)
      {
          if (...)
          {
              x = default;
              return false;
          }
          
          x = 42;
          return true;
      }
      
      if (TryGetValue(out int x))
      {
          Console.WriteLine(x);
      }
      

      in 則是在 C# 7 才引入的,相對于 ref 而言,in 提供了只讀引用的功能。通過 in 傳入的參數會通過引用方式進行只讀傳遞,類似 C++ 中的 const T*

      為了提升 in 的易用性,C# 為其加入了隱式引用傳遞的功能,即調用時不需要在調用處寫一個 in,編譯器會自動為你創建局部變量并傳遞對該變量的引用:

      void Foo(in Mat3x3 mat)
      {
          mat.X13 = 4.2f; // 錯誤,因為只讀引用不能修改
      }
      
      // 編譯后會自動創建一個局部變量保存這個 new 出來的 Mat3x3
      // 然后調用函數時會傳遞對該局部變量的引用
      Foo(new() {  }); 
      
      struct Mat3x3
      {
          public float X11, X12, X13, X21, X22, X23, X31, X32, X33;
      }
      

      當然,我們也可以像 ref 那樣使用 in,明確指出我們引用的是什么東西:

      Mat3x3 x = ...;
      Foo(in x);
      

      struct 默認的參數傳遞行為是傳遞值的拷貝,當傳遞的對象較大時(一般指多于 4 個字段的對象),就會發生比較大的拷貝開銷,此時只需要利用只讀引用的方法傳遞參數即可避免,提升程序的性能。

      從 C# 7 開始,我們可以在方法中返回引用,例如:

      ref int Foo(int[] array)
      {
          return ref array[3];
      }
      

      調用該函數時,如果通過 ref 方式調用,則會接收到返回的引用:

      int[] array = new[] { 1, 2, 3, 4, 5 };
      ref int x = ref Foo(array);
      Console.WriteLine(x); // 4
      x = 5;
      Console.WriteLine(array[3]); // 5
      

      否則表示接收值,與返回非引用沒有區別:

      int[] array = new[] { 1, 2, 3, 4, 5 };
      int x = Foo(array);
      Console.WriteLine(x); // 4
      x = 5;
      Console.WriteLine(array[3]); // 4
      

      與 C/C++ 的指針不同的是,C# 中通過 ref 顯式標記一個東西是否是引用,如果沒有標記 ref,則一定不會是引用。

      當然,配套而來的便是返回只讀引用,確保返回的引用是不可修改的。與 ref 一樣,ref readonly 也是可以作為變量來使用的:

      ref readonly int Foo(int[] array)
      {
          return ref array[3];
      }
      
      int[] array = new[] { 1, 2, 3, 4, 5 };
      ref readonly int x = ref Foo(array);
      x = 5; // 錯誤
      ref readonly int y = ref array[1];
      y = 3; // 錯誤
      

      ref struct

      C# 7.2 引入了一種新的類型:ref struct。這種類型由編譯器和運行時同時確保絕對不會被裝箱,因此這種類型的實例的生命周期非常明確,它只可能在棧內存中,而不可能出現在堆內存中:

      Foo[] foos = new Foo[] { new(), new() }; // 錯誤
      
      ref struct Foo
      {
          public int X;
          public int Y;
      }
      

      借助 ref struct,我們便能在 ref struct 中保存引用,而無需擔心 ref struct 的實例因為生命周期被意外延長而導致出現無效引用。

      Span<T>ReadOnlySpan<T>

      從 .NET Core 2.1 開始,.NET 引入了 Span<T>ReadOnlySpan<T> 這兩個類型來表示對一段連續內存的引用和只讀引用。

      Span<T>ReadOnlySpan<T> 都是 ref struct,因此他們絕對不可能被裝箱,這確保了只要在他們自身的生命周期內,他們所引用的內存絕對都是有效的,因此借助這兩個類型,我們可以代替指針來安全地操作任何連續內存。

      Span<int> x = new[] { 1, 2, 3, 4, 5 };
      x[2] = 0;
      
      void* ptr = NativeMemory.Alloc(1024);
      Span<int> y = new Span<int>(ptr, 1024 / sizeof(int));
      y[4] = 42;
      NativeMemory.Free(ptr);
      

      我們還可以在 foreach 中使用 refref readonly 來以引用的方式訪問各成員:

      Span<int> x = new[] { 1, 2, 3, 4, 5 };
      foreach (ref int i in x) i++;
      foreach (int i in x) Console.WriteLine(i); // 2 3 4 5 6
      

      stackalloc

      在 C# 中,除了 new 之外,我們還有一個關鍵字 stackalloc,允許我們在棧內存上分配數組:

      Span<int> array = stackalloc[] { 1, 2, 3, 4, 5 };
      

      這樣我們就成功在棧上分配出了一個數組,這個數組的生命周期就是所在代碼塊的生命周期。

      ref field

      我們已經能夠在局部變量中使用 refref readonly 了,自然,我們就想要在字段中也使用這些東西。因此我們在 C# 11 中迎來了 refref readonly 字段。

      字段的生命周期與包含該字段的類型的實例相同,因此,為了確保安全,refref readonly 必須在 ref struct 中定義,這樣才能確保這些字段引用的東西一定是有效的:

      int x = 1;
      
      Foo foo = new Foo(ref x);
      foo.X = 2;
      Console.WriteLine(x); // 2
      
      Bar bar = new Bar { X = ref foo.X };
      x = 3;
      Console.WriteLine(bar.X); // 3
      bar.X = 4; // 錯誤
      
      ref struct Foo
      {
          public ref int X;
          
          public Foo(ref int x)
          {
              X = ref x;
          }
      }
      
      ref struct Bar
      {
          public ref readonly int X;
      }
      

      當然,上面的 Bar 里我們展示了對只讀內容的引用,但是字段本身也可以是只讀的,于是我們就還有:

      ref struct Bar
      {
          public ref int X; // 引用可變內容的可變字段
          public ref readonly int Y; // 引用只讀內容的可變字段
          public readonly ref int Z; // 引用可變內容的只讀字段
          public readonly ref readonly int W; // 引用只讀內容的只讀字段
      }
      

      scopedUnscopedRef

      我們再看看上面這個例子的 Foo,這個 ref struct 中有接收引用作為參數的構造函數,這次我們不再在字段中保存引用:

      Foo Test()
      {
          Span<int> x = stackalloc[] { 1, 2, 3, 4, 5 };
          Foo foo = new Foo(ref x[0]); // 錯誤
          return foo;
      }
      
      ref struct Foo
      {
          public Foo(ref int x)
          {
              x++;
          }
      }
      

      你會發現這時代碼無法編譯了。

      因為 stackalloc 出來的東西僅在 Test 函數的生命周期內有效,但是我們有可能在 Foo 的構造函數中將 ref int x 這一引用存儲到 Foo 的字段中,然后由于 Test 方法返回了 foo,這使得 foo 的生命周期被擴展到了調用 Test 函數的函數上,有可能導致本身應該在 Test 結束時就釋放的 x[0] 的生命周期被延長,從而出現無效引用。因此編譯器拒絕編譯了。

      你可能會好奇,編譯器在理論上明明可以檢測到底有沒有實際的代碼在字段中保存了引用,為什么還是直接報錯了?這是因為,如果需要檢測則需要實現復雜度極其高的過程分析,不僅會大幅拖慢編譯速度,而且還存在很多無法靜態處理的邊緣情況。

      那要怎么處理呢?這個時候 scoped 就出場了:

      Foo Test()
      {
          Span<int> x = stackalloc[] { 1, 2, 3, 4, 5 };
          Foo foo = new Foo(ref x[0]);
          return foo;
      }
      
      ref struct Foo
      {
          public Foo(scoped ref int x)
          {
              x++;
          }
      }
      

      我們只需要在 ref 前加一個 scoped,顯式標注出 ref int x 的生命周期不會超出該函數,這樣我們就能通過編譯了。

      此時,如果我們試圖在字段中保存這個引用的話,編譯器則會有效的指出錯誤:

      ref struct Foo
      {
          public ref int X;
          public Foo(scoped ref int x)
          {
              X = ref x; // 錯誤
          }
      }
      

      同樣的,我們還可以在局部變量中配合 ref 或者 ref readonly 使用 scoped

      Span<int> a = stackalloc[] { 1, 2, 3, 4, 5 };
      scoped ref int x = ref a[0];
      scoped ref readonly int y = ref a[1];
      foreach (scoped ref int i in a) i++;
      foreach (scoped ref readonly int i in a) Console.WriteLine(i); // 2 3 4 5 6
      x++;
      Console.WriteLine(a[0]); // 3
      a[1]++;
      Console.WriteLine(y); // 4
      

      當然,上面這個例子中即使不加 scoped,也是默認 scoped 的,這里標出來只是為了演示,實際上與下面的代碼等價:

      Span<int> a = stackalloc[] { 1, 2, 3, 4, 5 };
      ref int x = ref a[0];
      ref readonly int y = ref a[1];
      foreach (ref int i in a) i++;
      foreach (ref readonly int i in a) Console.WriteLine(i); // 2 3 4 5 6
      x++;
      Console.WriteLine(a[0]); // 3
      a[1]++;
      Console.WriteLine(y); // 4
      

      對于 ref struct 而言,由于其自身就是一種可以保存引用的“類引用”類型,因此我們的 scoped 也可以用于 ref struct,表明該 ref struct 的生命周期就是當前函數:

      Span<int> Foo(Span<int> s)
      {
          return s;
      }
      
      Span<int> Bar(scoped Span<int> s)
      {
          return s; // 錯誤
      }
      

      有時候我們希望在 struct 中返回 this 上成員的引用,但是由于 structthis 有著默認的 scoped 生命周期,因此此時無法通過編譯。這個時候我們可以借助 [UnscopedRef] 來將 this 的生命周期從當前函數延長到調用函數上:

      Foo foo = new Foo();
      foo.RefX = 42;
      Console.WriteLine(foo.X); // 42
      
      struct Foo
      {
          public int X;
      
          [UnscopedRef]
          public ref int RefX => ref X;
      }
      

      這對 out 也是同理的,因為 out 也是默認有 scoped 生命周期:

      ref int Foo(out int i) 
      {
          i = 42;
          return ref i; // 錯誤
      }
      

      但是我們同樣可以添加 [UnscopedRef] 來擴展生命周期:

      ref int Foo([UnscopedRef] out int i) 
      {
          i = 42;
          return ref i;
      }
      

      UnsafeMarshalMemoryMarshalCollectionsMarshalNativeMemoryBuffer

      在 .NET 中,我們有著非常多的工具函數,分布在 Unsafe.*Marshal.*MemoryMarshal.*CollectionsMarshal.*NativeMemory.*Buffer.* 中。利用這些工具函數,我們可以非常高效地在幾乎不直接使用指針的情況下,操作各類內存、引用和數組、集合等等。當然,使用的前提是你有相關的知識并且明確知道你在干什么,不然很容易寫出不安全的代碼,畢竟這里面大多數 API 就是 unsafe 的。

      例如消除掉邊界檢查的訪問:

      void Foo(Span<int> s)
      {
          Console.WriteLine(Unsafe.Add(ref MemoryMarshal.GetReference(s), 3));
      }
      
      Span<int> s = new[] { 1, 2, 3, 4, 5, 6 };
      Foo(s); // 4
      

      查看生成的代碼驗證:

      G_M000_IG02:                ;; offset=0004H
             mov      rcx, bword ptr [rcx]
             mov      ecx, dword ptr [rcx+0CH]
             call     [System.Console:WriteLine(int)]
      

      可以看到,邊界檢查確實被消滅了,對比直接訪問的情況:

      void Foo(Span<int> s)
      {
          Console.WriteLine(s[3]);
      }
      
      G_M000_IG02:                ;; offset=0004H
             cmp      dword ptr [rcx+08H], 3 ; <-- range check
             jbe      SHORT G_M000_IG04
             mov      rcx, bword ptr [rcx]
             mov      ecx, dword ptr [rcx+0CH]
             call     [System.Console:WriteLine(int)]
             nop
      
      G_M000_IG04:                ;; offset=001CH
             call     CORINFO_HELP_RNGCHKFAIL
             int3
      

      再比如,直接獲取字典中成員的引用:

      Dictionary<int, int> dict = new()
      {
          [1] = 7,
          [2] = 42
      };
      
      // 如果存在則獲取引用,否則添加一個 default 進去然后再返回引用
      ref int value = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, 3, out bool exists);
      value++;
      Console.WriteLine(exists); // false
      Console.WriteLine(dict[3]); // 1
      

      如此一來,我們便不需要先調用 ContainsKey 再操作,只需要一次查找即可完成我們需要的操作,而不是 ContainsKey 查找一次,后續操作再查找一次。

      我們還可以用 Buffer.CopyMemory 來實現與 memcpy 等價的高效率數組拷貝;再有就是前文中出現過的 NativeMemory,借助此 API,我們可以手動分配非托管內存,并指定對齊方式、是否清零等參數。

      顯式布局、字段重疊和定長數組

      C# 的 struct 允許我們利用 [StructLayout] 按字節手動指定內存布局,例如:

      unsafe
      {
          Console.WriteLine(sizeof(Foo)); // 10
      }
      
      [StructLayout(LayoutKind.Explicit, Pack = 1)]
      struct Foo
      {
          [FieldOffset(0)] public int X;
          [FieldOffset(4)] public float Y;
          [FieldOffset(0)] public long XY;
          [FieldOffset(8)] public byte Z;
          [FieldOffset(9)] public byte W;
      }
      

      上面的例子中我們將 XYXY 的內存重疊,并且利用 Pack 指定了 padding 行為,使得 Foo 的長度為 10 字節,而不是 16 字節。

      我們還有定長數組:

      Foo foo = new Foo();
      foo.Array[1] = 42;
      
      struct Foo
      {
          public unsafe fixed int Array[4];
      }
      

      此時,我們便有一個長度固定為 4 的數組存在于 Foo 的字段中,占據 16 個字節的長度。

      接口的虛靜態方法

      .NET 7 中我們迎來了接口的虛靜態方法,這一特性加強了 C# 泛型的表達能力,使得我們可以更好地利用參數化多態來更高效地對代碼進行抽象。

      此前當遇到字符串時,如果我們想要編寫一個方法來對字符串進行解析,得到我們想要的類型的話,要么需要針對各種重載都編寫一份,要么寫成泛型方法,然后再在里面判斷類型。兩種方法編寫起來都非常的麻煩:

      int ParseInt(string str);
      long ParseLong(string str);
      float ParseFloat(string str);
      // ...
      

      或者:

      T Parse<T>(string str)
      {
          if (typeof(T) == typeof(int)) return int.Parse(str);
          if (typeof(T) == typeof(long)) return long.Parse(str);
          if (typeof(T) == typeof(float)) return float.Parse(str);
          // ...
      }
      

      盡管 JIT 有能力在編譯時消除掉多余的分支(因為 T 在編譯時已知),編寫起來仍然非常費勁,并且無法處理沒有覆蓋到的情況。

      但現在我們只需要利用接口的虛靜態方法,即可高效的對所有實現了 IParsable<T> 的類型實現這個 Parse 方法。.NET 標準庫中已經內置了不少相關類型,例如 System.IParsable<T> 的定義如下:

      public interface IParsable<TSelf> where TSelf : IParsable<TSelf>?
      {
          abstract static TSelf Parse(string s, IFormatProvider? provider);
          abstract static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result);
      }
      

      那么,我們只需要編寫一個:

      T Parse<T>(string str) where T : IParsable<T>
      {
          return T.Parse(str, null);
      }
      

      即可。

      這樣,哪怕是其他地方定義的類型,只要實現了 IParsable<T>,就能夠傳到這個方法中:

      struct Point : IParsable<Point>
      {
          public int X, Y;
          
          public static Point Parse(string s, IFormatProvider? provider) { ... }
          public static bool TryParse(string? s, IFormatProvider? provider, out Point result) { ... }
      }
      

      當然,既然是虛靜態方法,那就意味著不僅僅可以是 abstract,更可以是 virtual 的,如此一來我們還可以提供自己的默認實現:

      interface IFoo
      {
          virtual static void Hello() => Console.WriteLine("hello");
      }
      

      DisposeIDisposable

      我們有時需要顯式地手動控制資源釋放,而不是一味地交給 GC 來進行處理,那么此時我們的老朋友 Dispose 就派上用場了。

      對于 classstructrecord 而言,我們需要為其實現 IDisposable 接口,而對于 ref struct 而言,我們只需要暴露一個 public void Dispose()。這樣一來,我們便可以用 using 來自動進行資源釋放。

      例如:

      // 在 foo 的作用域結束時自動調用 foo.Dispose()
      using Foo foo = new Foo();
      // ...
      
      // 顯式指定 foo 的作用域
      using (Foo foo = new Foo())
      {
          // ...
      }
      
      struct Foo : IDisposable
      {
          private void* memory;
          private bool disposed;
          
          public void Dispose()
          {
              if (disposed) return;
              disposed = true;
              NativeMemory.Free(memory);
          }
      }
      

      異常處理的編譯優化

      異常是個好東西,但是也會對效率造成影響。因為異常在代碼中通常是不常見的,因為 JIT 在編譯代碼時,會將包含拋出異常的代碼認定為冷塊(即不會被怎么執行的代碼塊),這么一來會影響 inline 的決策:

      void Foo()
      {
          // ...
          throw new Exception();
      }
      

      例如上面這個 Foo 方法,就很難被 inline 掉。

      但是,我們可以將異常拿走放到單獨的方法中拋出,這么一來,拋異常的行為就被我們轉換成了普通的函數調用行為,于是就不會影響對 Foo 的 inline 優化,將冷塊從 Foo 轉移到了 Throw 中:

      [DoesNotReturn] void Throw() => throw new Exception();
      
      void Foo()
      {
          // ...
          Throw();
      }
      

      考慮到目前 .NET 還沒有 bottom types 和 union types,當我們的 Foo 需要返回東西的時候,很顯然上面的代碼會因為不是所有路徑都返回了東西而報錯,此時我們只需要將 Throw 的返回值類型改成我們想返回的類型,或者干脆封裝成泛型方法然后傳入類型參數即可。因為 throw 在 C# 中隱含了不會返回的含義,編譯器遇到 throw 時知道這個是不會返回的,也就不會因為 Throw 沒有返回東西而報錯:

      [DoesNotReturn] int Throw1() => throw new Exception();
      [DoesNotReturn] T Throw2<T>() => throw new Exception();
      
      int Foo1()
      {
          // ...
          return Throw1();
      }
      
      int Foo2()
      {
          // ...
          return Throw2<int>();
      }
      

      指針和函數指針

      指針相信大家都不陌生,像 C/C++ 中的指針那樣,C# 中套一個 unsafe 就能直接用。唯一需要注意的地方是,由于 GC 可能會移動堆內存上的對象,所以在使用指針操作 GC 堆內存中的對象前,需要先使用 fixed 將其固定:

      int[] array = new[] { 1, 2, 3, 4, 5 };
      fixed (int* p = array)
      {
          Console.WriteLine(*(p + 3)); // 4
      }
      

      當然,指針不僅僅局限于對象,函數也可以有函數指針:

      delegate* managed<int, int, int> f = &Add;
      Console.WriteLine(f(3, 4)); // 7
      static int Add(int x, int y) => x + y;
      

      函數指針也可以指向非托管方法,例如來自 C++ 庫中、有著 cdecl 調用約定的函數:

      delegate* unmanaged[Cdecl]<int, int, int> f = ...;
      

      進一步我們還可以指定 SuppressGCTransition 來取消做互操作時 GC 上下文的切換來提高性能。當然這是危險的,只有當被調用的函數能夠非常快完成時才能使用:

      delegate* unmanaged[Cdecl, SuppressGCTransition]<int, int, int> f = ...;
      

      SuppressGCTransition 同樣可以用于 P/Invoke:

      [DllImport(...), SuppressGCTransition]
      static extern void Foo();
      
      [LibraryImport(...), SuppressGCTransition]
      static partial void Foo();
      

      IntPtrUIntPtrnintnuint

      C# 中有兩個通過數值方式表示的指針類型:IntPtrUIntPtr,分別是有符號和無符號的,并且長度等于當前進程的指針類型長度。由于長度與平臺相關的特性,它也可以用來表示 native 數值,因此誕生了 nintnuint,底下分別是 IntPtrUIntPtr,類似 C++ 中的 ptrdiff_tsize_t 類型。

      這么一來我們就可以方便地像使用其他的整數類型那樣對 native 數值類型運算:

      nint x = -100;
      nuint y = 200;
      Console.WriteLine(x + (nint)y); //100
      

      當然,寫成 IntPtrUIntPtr 也是沒問題的:

      IntPtr x = -100;
      UIntPtr y = 200;
      Console.WriteLine(x + (IntPtr)y); //100
      

      SkipLocalsInit

      SkipLocalsInit 可以跳過 .NET 默認的分配時自動清零行為,當我們知道自己要干什么的時候,使用 SkipLocalsInit 可以節省掉內存清零的開銷:

      [SkipLocalsInit]
      void Foo1()
      {
          Guid guid;
          unsafe
          {
              Console.WriteLine(*(Guid*)&guid);
          }
      }
      
      void Foo2()
      {
          Guid guid;
          unsafe
          {
              Console.WriteLine(*(Guid*)&guid);
          }
      }
      
      Foo1(); // 一個不確定的 Guid
      Foo2(); // 00000000-0000-0000-0000-000000000000
      

      實際例子

      熟悉完 .NET 中的部分基礎設施,我們便可以來實際編寫一些代碼了。

      非托管內存

      在大型應用中,我們偶爾會用到超出 GC 管理能力范圍的超大數組(> 4G),當然我們可以選擇類似鏈表那樣拼接多個數組,但除了這個方法外,我們還可以自行封裝出一個處理非托管內存的結構來使用。另外,這種需求在游戲開發中也較為常見,例如需要將一段內存作為頂點緩沖區然后送到 GPU 進行處理,此時要求這段內存不能被移動。

      那此時我們可以怎么做呢?

      首先我們可以實現基本的存儲、釋放和訪問功能:

      public sealed class NativeBuffer<T> : IDisposable where T : unmanaged
      {
          private unsafe T* pointer;
          public nuint Length { get; }
      
          public NativeBuffer(nuint length)
          {
              Length = length;
              unsafe
              {
                  pointer = (T*)NativeMemory.Alloc(length * sizeof(T));
              }
          }
      
          public NativeBuffer(Span<T> span) : this((nuint)span.Length)
          {
              unsafe
              {
                  fixed (T* ptr = span)
                  {
                      Buffer.MemoryCopy(ptr, pointer, sizeof(T) * span.Length, sizeof(T) * span.Length);
                  }
              }
          }
      
          [DoesNotReturn] private ref T ThrowOutOfRange() => throw new IndexOutOfRangeException();
      
          public ref T this[nuint index]
          {
              get
              {
                  unsafe
                  {
                      return ref (index >= Length ? ref ThrowOutOfRange() : ref (*(pointer + index)));
                  }
              }
          }
      
          public void Dispose()
          {
              unsafe
              {
                  // 判斷內存是否有效
                  if (pointer != (T*)0)
                  {
                      NativeMemory.Free(pointer);
                      pointer = (T*)0;
                  }
              }
          }
      
          // 即使沒有調用 Dispose 也可以在 GC 回收時釋放資源
          ~NativeBuffer()
          {
              Dispose();
          }
      }
      

      如此一來,使用時只需要簡單的:

      NativeBuffer<int> buf = new(new[] { 1, 2, 3, 4, 5 });
      Console.WriteLine(buf[3]); // 4
      buf[2] = 9;
      Console.WriteLine(buf[2]); // 9
      // ...
      buf.Dispose();
      

      或者讓它在作用域結束時自動釋放:

      using NativeBuffer<int> buf = new(new[] { 1, 2, 3, 4, 5 });
      

      或者干脆不管了,等待 GC 回收時自動調用我們的編寫的析構函數,這個時候就會從 ~NativeBuffer 調用 Dispose 方法。

      緊接著,為了能夠使用 foreach 進行迭代,我們還需要實現一個 Enumerator,但是為了提升效率并且支持引用,此時我們選擇實現自己的 GetEnumerator

      首先我們實現一個 NativeBufferEnumerator

      public ref struct NativeBufferEnumerator
      {
          private unsafe readonly ref T* pointer;
          private readonly nuint length;
          private ref T current;
          private nuint index;
      
          public ref T Current
          {
              get
              {
                  unsafe
                  {
                      // 確保指向的內存仍然有效
                      if (pointer == (T*)0)
                      {
                          return ref Unsafe.NullRef<T>();
                      }
                      else return ref current;
                  }
              }
          }
      
          public unsafe NativeBufferEnumerator(ref T* pointer, nuint length)
          {
              this.pointer = ref pointer;
              this.length = length;
              this.index = 0;
              this.current = ref Unsafe.NullRef<T>();
          }
      
          public bool MoveNext()
          {
              unsafe
              {
                  // 確保沒有越界并且指向的內存仍然有效
                  if (index >= length || pointer == (T*)0)
                  {
                      return false;
                  }
                  
                  if (Unsafe.IsNullRef(ref current)) current = ref *pointer;
                  else current = ref Unsafe.Add(ref current, 1);
              }
              index++;
              return true;
          }
      }
      

      然后只需要讓 NativeBuffer.GetEnumerator 方法返回我們的實現好的迭代器即可:

      public NativeBufferEnumerator GetEnumerator()
      {
          unsafe
          {
              return new(ref pointer, Length);
          }
      }
      

      從此,我們便可以輕松零分配地迭代我們的 NativeBuffer 了:

      int[] buffer = new[] { 1, 2, 3, 4, 5 };
      using NativeBuffer<int> nb = new(buffer);
      foreach (int i in nb) Console.WriteLine(i); // 1 2 3 4 5
      foreach (ref int i in nb) i++;
      foreach (int i in nb) Console.WriteLine(i); // 2 3 4 5 6
      

      并且由于我們的迭代器中保存著對 NativeBuffer.pointer 的引用,如果 NativeBuffer 被釋放了,運行了一半的迭代器也能及時發現并終止迭代:

      int[] buffer = new[] { 1, 2, 3, 4, 5 };
      NativeBuffer<int> nb = new(buffer);
      foreach (int i in nb)
      {
          Console.WriteLine(i); // 1
          nb.Dispose();
      }
      

      結構化數據

      我們經常會需要存儲結構化數據,例如在進行圖片處理時,我們經常需要保存顏色信息。這個顏色可能是直接從文件數據中讀取得到的。那么此時我們便可以封裝一個 Color 來代表顏色數據 RGBA:

      [StructLayout(LayoutKind.Sequential)]
      public struct Color : IEquatable<Color>
      {
          public byte R, G, B, A;
      
          public Color(byte r, byte g, byte b, byte a = 0)
          {
              R = r;
              G = g;
              B = b;
              A = a;
          }
      
          public override int GetHashCode() => HashCode.Combine(R, G, B, A);
          public override string ToString() => $"Color {{ R = {R}, G = {G}, B = {B}, A = {A} }}";
          public override bool Equals(object? other) => other is Color color ? Equals(color) : false;
          public bool Equals(Color other) => (R, G, B, A) == (other.R, other.G, other.B, other.A);
      }
      

      這么一來我們就有能表示顏色數據的類型了。但是這么做還不夠,我們需要能夠和二進制數據或者字符串編寫的顏色值相互轉換,因此我們編寫 SerializeDeserializeParse 方法來進行這樣的事情:

      [StructLayout(LayoutKind.Sequential)]
      public struct Color : IParsable<Color>, IEquatable<Color>
      {
          public static byte[] Serialize(Color color)
          {
              unsafe
              {
                  byte[] buffer = new byte[sizeof(Color)];
                  MemoryMarshal.Write(buffer, ref color);
                  return buffer;
              }
          }
      
          public static Color Deserialize(ReadOnlySpan<byte> data)
          {
              return MemoryMarshal.Read<Color>(data);
          }
      
          [DoesNotReturn] private static void ThrowInvalid() => throw new InvalidDataException("Invalid color string.");
          
          public static Color Parse(string s, IFormatProvider? provider = null)
          {
              if (s.Length is not 7 and not 9 || (s.Length > 0 && s[0] != '#'))
              {
                  ThrowInvalid();
              }
              
              return new()
              {
                  R = byte.Parse(s[1..3], NumberStyles.HexNumber, provider),
                  G = byte.Parse(s[3..5], NumberStyles.HexNumber, provider),
                  B = byte.Parse(s[5..7], NumberStyles.HexNumber, provider),
                  A = s.Length is 9 ? byte.Parse(s[7..9], NumberStyles.HexNumber, provider) : default
              };
          }
          
          public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Color result)
          {
              result = default;
              if (s?.Length is not 7 and not 9 || (s.Length > 0 && s[0] != '#'))
              {
                  return false;
              }
      
              Color color = new Color();
              return byte.TryParse(s[1..3], NumberStyles.HexNumber, provider, out color.R)
                  && byte.TryParse(s[3..5], NumberStyles.HexNumber, provider, out color.G)
                  && byte.TryParse(s[5..7], NumberStyles.HexNumber, provider, out color.B)
                  && (s.Length is 9 ? byte.TryParse(s[7..9], NumberStyles.HexNumber, provider, out color.A) : true);
          }
      }
      

      接下來,我們再實現一個 ColorView,允許以多種方式對 Color 進行訪問和修改:

      public ref struct ColorView
      {
          private readonly ref Color color;
      
          public ColorView(ref Color color)
          {
              this.color = ref color;
          }
      
          [DoesNotReturn] private static ref byte ThrowOutOfRange() => throw new IndexOutOfRangeException();
      
          public ref byte R => ref color.R;
          public ref byte G => ref color.G;
          public ref byte B => ref color.B;
          public ref byte A => ref color.A;
          public ref uint Rgba => ref Unsafe.As<Color, uint>(ref color);
          public ref byte this[int index]
          {
              get
              {
                  switch (index)
                  {
                      case 0:
                          return ref color.R;
                      case 1:
                          return ref color.G;
                      case 2:
                          return ref color.B;
                      case 3:
                          return ref color.A;
                      default:
                          return ref ThrowOutOfRange();
                  }
              }
          }
      
          public ColorViewEnumerator GetEnumerator()
          {
              return new(this);
          }
      
          public ref struct ColorViewEnumerator
          {
              private readonly ColorView view;
              private int index;
      
              public ref byte Current => ref view[index];
      
              public ColorViewEnumerator(ColorView view)
              {
                  this.index = -1;
                  this.view = view;
              }
      
              public bool MoveNext()
              {
                  if (index >= 3) return false;
                  index++;
                  return true;
              }
          }
      }
      

      然后我們給 Color 添加一個 CreateView() 方法即可:

      public ColorView CreateView() => new(ref this);
      

      如此一來,我們便能夠輕松地通過不同視圖來操作 Color 數據,并且一切抽象都是零開銷的:

      Console.WriteLine(Color.Parse("#FFEA23")); // Color { R = 255, G = 234, B = 35, A = 0 }
      
      Color color = new(255, 128, 42, 137);
      ColorView view = color.CreateView();
      
      Console.WriteLine(color); // Color { R = 255, G = 128, B = 42, A = 137 }
      
      view.R = 7;
      view[3] = 28;
      Console.WriteLine(color); // Color { R = 7, G = 128, B = 42, A = 28 }
      
      view.Rgba = 3072;
      Console.WriteLine(color); // Color { R = 0, G = 12, B = 0, A = 0 }
      
      foreach (ref byte i in view) i++;
      Console.WriteLine(color); // Color { R = 1, G = 13, B = 1, A = 1 }
      

      后記

      C# 是一門自動擋手動擋同時具備的語言,上限極高的同時下限也極低。可以看到上面的幾個例子中,盡管封裝所需要的代碼較為復雜,但是到了使用的時候就如同一切的底層代碼全都消失了一樣,各種語法糖加持之下,不僅僅用起來非常的方便快捷,而且借助零開銷抽象,代碼的內存效率和運行效率都能達到 C++、Rust 的水平。此外,現在的 .NET 7 有了 NativeAOT 之后更是能直接編譯到本機代碼,運行時無依賴也完全不需要虛擬機,實現了與 C++、Rust 相同的應用形態。這些年來 .NET 在不同的平臺、不同工作負載上均有著數一數二的運行效率表現的理由也是顯而易見的。

      而代碼封裝的臟活則是由各庫的作者來完成的,大多數人在進行業務開發時,無需接觸和關系這些底層的東西,甚至哪怕什么都不懂都可以輕松使用封裝好的庫,站在這些低開銷甚至零開銷的抽象基礎之上來進行應用的構建。

      以上便是對 .NET 中進行零開銷抽象的一些簡單介紹,在開發中的局部熱點利用這些技巧能夠大幅度提升運行效率和內存效率。

      posted @ 2022-11-01 16:35  hez2010  閱讀(7262)  評論(48)    收藏  舉報
      主站蜘蛛池模板: 内丘县| 亚洲精品一区二区三区四区乱码| 丰满熟妇人妻av无码区| 一区二区三区四区五区自拍| 精品国产成人午夜福利| 99久久精品费精品国产一区二| 亚洲午夜理论片在线观看| 国产精品多p对白交换绿帽| 又大又粗又硬又爽黄毛少妇| 国产精品中文字幕av| 亚洲精品无码高潮喷水在线| 亚洲国产中文字幕在线视频综合| 日韩国产精品一区二区av| 色爱综合另类图片av| 国产成人无码免费视频在线| 一区二区三区日本久久九| 日韩精品亚洲专在线电影| 亚洲热妇无码av在线播放| 久久88香港三级台湾三级播放| 国产亚洲精品黑人粗大精选| 国产精品一区二区av片| 成人3d动漫一区二区三区| 91亚洲国产成人精品性色| 一区二区三区四区五区自拍| 搡老熟女老女人一区二区| 国产精品国产精品无卡区| 99精品久久精品| 亚洲香蕉网久久综合影视| 国产成人精品永久免费视频| 高清无码爆乳潮喷在线观看| 婷婷久久综合九色综合88| 深夜免费av在线观看| 亚洲一区二区三区在线观看精品中文| 久久视频这里只精品| 白嫩人妻精品一二三四区| 日本中文一二区有码在线| 亚洲国产良家在线观看| 国产欧美一区二区精品久久久| 亚洲一区二区三区18禁| 国产睡熟迷奷系列网站| 久久国产成人午夜av影院|