C# 使用SIMD向量類型加速浮點數組求和運算(4):用引用代替指針, 擺脫unsafe關鍵字,兼談Unsafe類的使用

来源:https://www.cnblogs.com/zyl910/archive/2023/07/30/dotnet_simd_BenchmarkVector4.html
-Advertisement-
Play Games

在C#中,數據類型分為值類型和引用類型兩種。 引用類型變數存儲的是數據的引用,數據存儲在數據堆中,而值類型變數直接存儲數據。對於引用類型,兩個變數可以引用同一個對象。因此,對一個變數的操作可能會影響另一個變數引用的對象。對於值類型,每個變數都有自己的數據副本,並且對一個變數的操作不可能影響另一個變數 ...


作者: zyl910

目錄

一、引言

C#沒有直接提供對數據進行重新解釋(C++的 reinterpret_cast)的功能,而在使用向量類型時,經常需要做這種操作。例如 第2篇文章,用了3種辦法——

  1. 事先將基元類型數組轉為了向量類型數組(SumVectorAvx).
  2. 使用Span改進數據載入(SumVectorAvxSpan).
  3. 使用指針改進數據載入(SumVectorAvxPtr).

第1種辦法其實是將全部數據搬運了一遍,開銷大。不適合生產使用,僅能用於教學演示。
剩下2種辦法雖然能用,但還是存在一些缺點的——

  • Span:Span的長度(Length)是32位有符號整數(Int32),且索引一般不能為負數,導致它的地址範圍只有31位,難以處理超過2GB的數據。而現在64位平臺已經普及了,有時存在“處理超過2GB的數據”的需求。官方說了“我們決定將其保留為 int”(We decided to keep it as an int),Span的位數升級無望。
  • 指針:雖然在C#中能夠使用指針語法,但是只能在用unsafe關鍵字申明的“非安全代碼”里使用,且項目配置里需啟用“允許非安全代碼”(Allow unsafe code),使用比較繁瑣。而且對於開發類庫等嚴格審查的場合,有時會規定不啟用“允許非安全代碼”。其次C#的指針還存在 “不支持泛型”、“有時需要fixed”等缺點。

有沒有更好的辦法呢?

二、辦法說明

最開始毫無頭緒,直到我分析了Span的源碼後,才發現C#如今能用引用來做重新解釋,從而解決上述難題。
而且如今引用的功能非常強大,能完全代替指針操作,且能擺脫unsafe關鍵字。適合不啟用“允許非安全代碼”等嚴格的場合。

2.1 歷史

C# 1.0 就支持了“ref”關鍵字,但僅能用於參數列表,用來表示“引用方式傳參”。
C# 7.0 新增了“局部引用和引用返回”(Ref locals and returns)特性。自此C#的引用(ref)與C++的引用(&),功能很接近了.
C# 7.3 新增了“重新分配局部引用變數”(Reassign ref local variables)特性。C#的引用(ref)與C++的引用(&),幾乎功能一致了.
再加上Unsafe類提供了 引用地址調整、引用地址比較、重新解釋(C++的 reinterpret_cast)、引用取消只讀(類似C++的 const_cast) 等功能. 使得C# 引用(ref)非常強大,能夠完全代替指針(*)操作,徹底擺脫unsafe關鍵字。
引用比指針還多了這些優點——

  • 指針不支持泛型,而引用是支持泛型的。
  • 指針有時需要fixed,而引用無需使用fixed。

可以簡單理解為這樣——如今C#里的引用,比指針的功能更強大。
於是在一些官方文檔中,將引用(ref)稱呼為“托管指針”(Managed pointer);並將傳統的指針(*),稱呼為“非托管指針”(Unmanaged pointer)。

2.2 局部引用變數與引用所指的值(類似指針的 地址運算符&、間接運算符*

摘自官方文檔。

https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/declarations#reference-variables
Reference 變數
聲明局部變數併在變數類型之前添加 ref 關鍵字時,聲明 reference 變數,或 ref 局部變數:
ref int alias = ref variable;

reference 變數是引用另一個變數(稱為引用)的變數。 也就是說,reference 變數是其引用的別名。 向 reference 變數賦值時,該值將分配給引用。 讀取 reference 變數的值時,將返回引用的值。 以下示例演示了該行為:
int a = 1;
ref int alias = ref a;
Console.WriteLine($"(a, alias) is ({a}, {alias})");  // output: (a, alias) is (1, 1)
a = 2;
Console.WriteLine($"(a, alias) is ({a}, {alias})");  // output: (a, alias) is (2, 2)
alias = 3;
Console.WriteLine($"(a, alias) is ({a}, {alias})");  // output: (a, alias) is (3, 3)

上述代碼對應的C語言版代碼如下。

int a = 1;
int* alias = &a;
printf("(a, alias) is (%d, %d)", a, *alias);	// output: (a, alias) is (1, 1)
a = 2;
printf("(a, alias) is (%d, %d)", a, *alias);	// output: (a, alias) is (2, 2)
*alias = 3;
printf("(a, alias) is (%d, %d)", a, *alias);	// output: (a, alias) is (3, 3)

主要是以下幾點的寫法不同:

  1. ref int alias與C語言的int* alias: 聲明變數時,類型的前面可加上“ref”關鍵字,表示它是引用類型。類似C語言的變數類型加“*”,表示它是指針類型。
  2. alias = ref a與C語言的alias = &a: 將引用指向某個變數時,需要給右式中變數的前面加上“ref”關鍵字。類似指針的 地址運算符&
  3. 賦值時的alias與C語言的*alias: 引用在直接賦值時,都是讀取或設置其指向的值(並不是更改引用的指向)。而指針須使用 間接運算符*

引用可以直接用“.”運算符來訪問成員,不像指針那樣還需使用“指針成員訪問運算符->”。

2.3 重新分配局部引用變數(類似指針直接賦值)

上面提到引用在直接賦值時,都是讀取或設置其指向的值。那麼怎樣才能更改引用的指向呢?
C# 7.3 新增了“重新分配局部引用變數”(Reassign ref local variables)特性,能夠實現該功能。
摘自官方文檔:

使用 ref 賦值運算符= ref 更改 reference 變數的引用,如以下示例所示:

void Display(int[] s) => Console.WriteLine(string.Join(" ", s));

int[] xs = { 0, 0, 0 };
Display(xs);

ref int element = ref xs[0];
element = 1;
Display(xs);

element = ref xs[^1];
element = 3;
Display(xs);
// Output:
// 0 0 0
// 1 0 0
// 1 0 3

在前面的示例中,element reference 變數初始化為第一個數組元素的別名。 然後,ref 將被重新分配,以引用最後一個數組元素。

上述代碼對應的C語言版代碼如下。

int[] xs = { 0, 0, 0 };
Display(xs);

int* element = &xs[0];
*element = 1;
Display(xs);

element = &xs[(sizeof(xs)/sizeof(xs[0]))-1];
*element = 3;
Display(xs);

規律是——

  • =的右邊若有“ref”關鍵字:更改引用的指向。類似指針直接賦值。
  • =的右邊若沒有“ref”關鍵字:設置其指向的值。類似指針的 間接運算符*

上述示例中,演示了引用對數組的訪問。C#中的指針若想訪問數組,一般得使用fixed關鍵字,而引用不用。
註:上述示例還用到了C# 8.0 的一個特性—— 從末尾開始索引運算符^^1 就是指末尾的最後1個元素,於是在C語言中是 (sizeof(xs)/sizeof(xs[0]))-1 .

2.4 引用地址調整(類似指針加減法)

指針能通過加減法來調整地址,從而對一片連續的記憶體進行操作。
C#的引用也能夠調整地址,故也能對一片連續的記憶體進行操作。Unsafe類提供了的Add等方法來調整引用的地址,例如以下方法。

Add<T>(T, Int32)	將偏移量添加到給定的托管指針。
Add<T>(T, IntPtr)	將元素偏移量添加到給定的托管指針。
AddByteOffset<T>(T, IntPtr)	將位元組偏移量添加到給定的托管指針。
Subtract<T>(T, Int32)	從給定的托管指針中減去偏移量。
Subtract<T>(T, IntPtr)	從給定的托管指針中減去元素偏移量。
SubtractByteOffset<T>(T, IntPtr)	從給定的托管指針中減去位元組偏移量。

帶“ByteOffset”尾碼的方法,是以位元組為單位的。而沒有該尾碼的方法,是以元素大小為單位的,類似指針加減法。

2.5 引用地址比較(類似指針比較)

指針能夠進行比較。
引用也能夠進行比較。Unsafe類提供了的AreSame等方法來獲得比較結果,例如以下方法。

AreSame<T>(T, T)	確定指定的托管指針是否指向同一位置。
IsAddressGreaterThan<T>(T, T)	返回一個值,該值指示指定的托管指針是否大於另一個指定的托管指針。
IsAddressLessThan<T>(T, T)	返回一個值,該值指示指定的托管指針是否小於另一個指定的托管指針。

2.6 重新解釋(類似C++的 reinterpret_cast)

C++的 reinterpret_cast運算符能夠對數據進行重新解釋。且在C語言(或C#的非安全代碼)中,可以通過對指針進行強制類型轉換,從而對數據進行重新解釋。
對於C#的引用,可以用Unsafe類的As方法對數據進行重新解釋。
摘自官方文檔。

As<TFrom,TTo>(TFrom)
將給定的托管指針重新解釋為指向 TTo類型的新托管指針。

public static ref TTo As<TFrom,TTo> (ref TFrom source);

類型參數
TFrom	要重新解釋的托管指針的類型。
TTo	托管指針的所需類型。

參數
source	TFrom	用於重新解釋的托管指針。

返回
TTo	指向 TTo類型的新托管指針。

註解
此 API 在概念上類似於 C++ 的 `reinterpret_cast<>`。 調用方有責任確保強制轉換是合法的。 不會執行運行時檢查。
僅重新解釋托管指針。 引用的值本身將保持不變。 請考慮以下示例。


int[] intArray = new int[] { 0x1234_5678 }; // a 1-element array
ref int refToInt32 = ref intArray[0]; // managed pointer to first Int32 in array
ref short refToInt16 = ref Unsafe.As<int, short>(ref refToInt32); // reinterpret as managed pointer to Int16
Console.WriteLine($"0x{refToInt16:x4}");

此程式的輸出取決於當前電腦的端序。 在 big-endian 體繫結構中,此代碼輸出 0x1234。 在 little-endian 體繫結構上,此代碼輸出 0x5678。

將托管指針從較窄的類型轉換為較寬的類型時,調用方必須確保取消引用指針不會產生超出邊界的訪問。 調用方還負責確保生成的指針正確對齊所引用的類型。
當將托管指針從較窄類型強制轉換為較寬類型時,調用方必須確保對指針的解引用不會導致越界訪問。調用者還負責確保結果指針為引用類型正確對齊。
 有關對齊假設的詳細信息,請參閱 ECMA-335,第 I.12.6.2 (“對齊”) 。

2.7 引用取消只讀(類似C++的 const_cast)

在C#中可以用“ref readonly”定義只讀引用。摘自官方文檔。

可以定義 ref readonly 局部變數。 不能為 ref readonly 變數賦值。 但是,可以 ref 重新分配這樣的 reference 變數,如以下示例所示:

int[] xs = { 1, 2, 3 };

ref readonly int element = ref xs[0];
// element = 100;  error CS0131: The left-hand side of an assignment must be a variable, property or indexer
Console.WriteLine(element);  // output: 1

element = ref xs[^1];
Console.WriteLine(element);  // output: 3

可見,在使用“= ref”做引用賦值時,能方便的將“可變引用(ref)”賦值給“只讀引用(ref readonly)”。因為將“能讀寫變數”限製為“只讀變數”,是安全的。
但反向該如何做呢,即怎樣將“只讀引用(ref readonly)”轉為“可變引用(ref)”?
雖然這個操作是不安全的,但是有些時候必須使用。類似C++提供const_cast運演算法,能夠去掉“const”。
Unsafe類的許多方法是僅支持“可變引用(ref)”的,例如“Add”等。當想對“只讀引用(ref readonly)”使用Unsafe類的方法前,需要將“只讀引用(ref readonly)”轉為“可變引用(ref)”。

解決辦法就是使用 Unsafe的AsRef方法。摘自官方文檔。

AsRef<T>(T)
將給定的只讀引用重新解釋為可變引用。
public static ref T AsRef<T> (scoped in T source);

類型參數
T	引用的基礎類型。

參數
source	T	要重新解釋的只讀引用。

返回
T	對類型的 T值的可變引用。

註解
此 API 在概念上類似於 C++的`const_cast<>`。調用方負責確保不會將數據寫入引用的位置。 運行時包含基於只讀引用真正不可變的假設的內部邏輯,違反此固定項的調用方可能會在運行時內觸發未定義的行為。

AsRef 通常用於將只讀引用傳遞給方法,例如 Add,接受可變托管指針作為參數。 請看下麵的示例。

int ComputeSumOfElements(ref int refToFirstElement, nint numElements)
{
  int sum = 0;
  for (nint i = 0; i < numElements; i++)
  {
    sum += Unsafe.Add(ref refToFirstElement, i);
  }
}

如果輸入參數不是 `ref int refToFirstElement`, 而是 `ref readonly int refToFirstElement`,則上一個示例不會編譯,因為不能將只讀引用用作Add的參數。相反,AsRef 可用於刪除不可變性約束並允許編譯成功,如以下示例所示。

int ComputeSumOfElements(ref readonly int refToFirstElement, nint numElements)
{
  int sum = 0;
  for (nint i = 0; i < numElements; i++)
  {
    sum += Unsafe.Add(ref Unsafe.AsRef(ref refToFirstElement), i);
  }
}

三、將指針代碼改寫為引用代碼

3.1 代碼編寫

我們先來回顧一下指針版的代碼。

private static float SumVectorAvxPtr(float[] src, int count, int loops) {
#if Allow_Intrinsics && UNSAFE
    unsafe {
        float rt = 0; // Result.
        int VectorWidth = Vector256<float>.Count; // Block width.
        int nBlockWidth = VectorWidth; // Block width.
        int cntBlock = count / nBlockWidth; // Block count.
        int cntRem = count % nBlockWidth; // Remainder count.
        Vector256<float> vrt = Vector256<float>.Zero; // Vector result.
        Vector256<float> vload;
        float* p; // Pointer for src data.
        int i;
        // Body.
        fixed(float* p0 = &src[0]) {
            for (int j = 0; j < loops; ++j) {
                p = p0;
                // Vector processs.
                for (i = 0; i < cntBlock; ++i) {
                    vload = Avx.LoadVector256(p);    // Load. vload = *(*__m256)p;
                    vrt = Avx.Add(vrt, vload);    // Add. vrt += vsrc[i];
                    p += nBlockWidth;
                }
                // Remainder processs.
                for (i = 0; i < cntRem; ++i) {
                    rt += p[i];
                }
            }
        }
        // Reduce.
        for (i = 0; i < VectorWidth; ++i) {
            rt += vrt.GetElement(i);
        }
        return rt;
    }
#else
    throw new NotSupportedException();
#endif
}

利用本文上面的知識,可以去掉unsafe關鍵字,將指針改造為引用。代碼如下。

private static float SumVectorAvxRef(float[] src, int count, int loops) {
#if Allow_Intrinsics
    float rt = 0; // Result.
    int VectorWidth = Vector256<float>.Count; // Block width.
    int nBlockWidth = VectorWidth; // Block width.
    int cntBlock = count / nBlockWidth; // Block count.
    int cntRem = count % nBlockWidth; // Remainder count.
    Vector256<float> vrt = Vector256<float>.Zero; // Vector result.
    int i;
    // Body.
    for (int j = 0; j < loops; ++j) {
        ref Vector256<float> p0 = ref Unsafe.As<float, Vector256<float>> (ref src[0]); // Pointer for src data.
        // Vector processs.
        for (i = 0; i < cntBlock; ++i) {
            vrt = Avx.Add(vrt, p0);    // Add. vrt += vsrc[i];
            p0 = ref Unsafe.Add(ref p0, 1);
        }
        // Remainder processs.
        ref float p = ref Unsafe.As<Vector256<float>, float>(ref p0);
        for (i = 0; i < cntRem; ++i) {
            rt += Unsafe.Add(ref p, i);
        }
    }
    // Reduce.
    for (i = 0; i < VectorWidth; ++i) {
        rt += vrt.GetElement(i);
    }
    return rt;
#else
    throw new NotSupportedException();
#endif
}

在向量處理(Vector processs)階段,需要向量類型的引用,於是定義了 ref Vector256<float> p0。因為C#里訪問的引用,會相當於訪問所指向的值,類似自動使用了指針的“間接運算符*”。即給Avx.Add傳遞的p0,是符合參數要求的值。
在餘數處理(Remainder processs)階段,需要基元類型的引用,於是定義了 ref float p。由於向量處理的迴圈結束時,p0正好移動到首個餘數的位置,所以可以用 Unsafe.As 進行重新解釋,為p賦值(設置引用的指向)。

3.2 測試結果

在我的電腦(lntel(R) Core(TM) i5-8250U CPU @ 1.60GHz、Windows 10)上運行時,x64、Release版程式的輸出信息為:

BenchmarkVectorCore30

IsRelease:      True
EnvironmentVariable(PROCESSOR_IDENTIFIER):      Intel64 Family 6 Model 142 Stepping 10, GenuineIntel
Environment.ProcessorCount:     8
Environment.Is64BitOperatingSystem:     True
Environment.Is64BitProcess:     True
Environment.OSVersion:  Microsoft Windows NT 6.2.9200.0
Environment.Version:    3.0.3
RuntimeEnvironment.GetRuntimeDirectory: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.3\
RuntimeInformation.FrameworkDescription:        .NET Core 3.0.3
BitConverter.IsLittleEndian:    True
IntPtr.Size:    8
Vector.IsHardwareAccelerated:   True
Vector<byte>.Count:     32      # 256bit
Vector<float>.Count:    8       # 256bit
Vector<double>.Count:   4       # 256bit
Vector4.Assembly.CodeBase:      file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/3.0.3/System.Numerics.Vectors.dll
Vector<T>.Assembly.CodeBase:    file:///C:/Program Files/dotnet/shared/Microsoft.NETCore.App/3.0.3/System.Private.CoreLib.dll

Benchmark:      count=4096, loops=1000000, countMFlops=4096
SumBase:        6.871948E+10    # msUsed=4891, MFLOPS/s=837.4565528521774
SumBaseU4:      2.748779E+11    # msUsed=1844, MFLOPS/s=2221.2581344902387, scale=2.65238611713666
SumVector4:     2.748779E+11    # msUsed=1219, MFLOPS/s=3360.1312551271535, scale=4.012305168170632
SumVector4U4:   1.0995116E+12   # msUsed=515, MFLOPS/s=7953.398058252427, scale=9.497087378640778
SumVectorT:     5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.018032786885247
SumVectorTU4:   2.1990233E+12   # msUsed=187, MFLOPS/s=21903.74331550802, scale=26.155080213903744
SumVectorAvx:   5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.0311986863711
SumVectorAvxSpan:       5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.018032786885247
SumVectorAvxRef:        5.497558E+11    # msUsed=609, MFLOPS/s=6725.7799671592775, scale=8.0311986863711
SumVectorAvxPtr:        5.497558E+11    # msUsed=610, MFLOPS/s=6714.754098360656, scale=8.018032786885247
SumVectorAvxU4: 2.1990233E+12   # msUsed=328, MFLOPS/s=12487.80487804878, scale=14.911585365853659
SumVectorAvxSpanU4:     2.1990233E+12   # msUsed=312, MFLOPS/s=13128.205128205129, scale=15.676282051282053
SumVectorAvxPtrU4:      2.1990233E+12   # msUsed=172, MFLOPS/s=23813.95348837209, scale=28.436046511627907
SumVectorAvxPtrU16:     8.386202E+12    # msUsed=188, MFLOPS/s=21787.23404255319, scale=26.01595744680851
SumVectorAvxPtrU16A:    8.3862026E+12   # msUsed=250, MFLOPS/s=16384, scale=19.564
SumVectorAvxPtrUX[4]:   2.1990233E+12   # msUsed=531, MFLOPS/s=7713.747645951035, scale=9.210922787193974
SumVectorAvxPtrUX[8]:   4.3980465E+12   # msUsed=500, MFLOPS/s=8192, scale=9.782
SumVectorAvxPtrUX[16]:  8.3862026E+12   # msUsed=484, MFLOPS/s=8462.809917355371, scale=10.105371900826446

可以看出 SumVectorAvxRef與SumVectorAvxPtr的性能幾乎一致。引用版演算法有時更快,可能是因為避免了fixed。
而且從“標準庫中的Span是靠引用實現的”來看,.NET運行時在執行引用相關演算法時,肯定是做了充足的編譯優化的。使其能達到與指針版演算法相當的性能。

四、小結

如今引用的功能非常強大,能完全代替指針操作,且能擺脫unsafe關鍵字。適合不啟用“允許非安全代碼”等嚴格的場合。

但需註意,引用有這些缺點——

  • 有些場合的編碼比指針繁瑣。例如用 Unsafe.As 做重新解釋時,還需要寫上源數據的類型。且不支持指針元素訪問運算符 [],只能用Unsafe.Add等方法對地址進行調整。
  • Unsafe的方法一般是沒有安全檢查的,這樣避免了性能損耗,但會留下一些安全隱患需開發者處理。若開發者處理不慎,會像開髮指針代碼那樣易出現 地址越界、懸空指針 等Bug。故需要開發者有指針開發經驗,細心避免bug。

源碼地址——
https://github.com/zyl910/BenchmarkVector/tree/main/BenchmarkVector

參考文獻

作者:zyl910 出處:http://www.cnblogs.com/zyl910/ 版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0.
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # Java 中 == 與 equals() 的區別 # 1. == ## == 是一個比較運算符,在使用時有可以判斷兩種情況 > ## 在用於基本類型時,即判斷兩邊數據的值是否相等。 > > ## 在用於引用類型時,即判斷兩邊是否為同一個對象即有相同的地址。 # 2. equals() 方法 ## ...
  • 原文在[這裡](https://go.dev/security/vuln/) ## 概述 Go幫助開發人員檢測、評估和解決可能被攻擊者利用的錯誤或弱點。在幕後,Go團隊運行一個管道來整理關於漏洞的報告,這些報告存儲在Go漏洞資料庫中。各種庫和工具可以讀取和分析這些報告,以瞭解特定用戶項目可能受到的影 ...
  • ## 教程簡介 Django是一個開放源代碼的Web應用框架,由Python寫成。採用了MTV的框架模式,即模型M,視圖V和模版T。它最初是被開發來用於管理勞倫斯出版集團旗下的一些以新聞內容為主的網站的,即是CMS(內容管理系統)軟體。Django是高水準的Python編程語言驅動的一個開源模型.視 ...
  • 《quarkus依賴註入》系列聚焦quarkus框架下bean的創建、使用、配置等場景的知識點,本文是系列的開篇,介紹CDI,實戰創建bean ...
  • 本篇介紹的是`pandas`選擇列數據的一個小技巧。之前已經介紹了很多選擇列數據的方式,比如`loc`,`iloc`函數,按列名稱選擇,按條件選擇等等。 這次介紹的是按照列的**數據類型**來選擇列,按類型選擇列可以幫助你快速選擇正確的數據類型,提高數據分析的效率。 # 1. 類型種類 `panda ...
  • 1.前言 眾所周知,Java是一門跨平臺語言,針對不同的操作系統有不同的實現。本文從一個非常簡單的api調用來看看Java具體是怎麼做的. 2.源碼分析 從FileInputStream.java中看到readBytes最後是native調用 /** * Reads a subarray as a ...
  • # RabbitMQ延時隊列和死信隊列 # 延時隊列和死信隊列 > 延時隊列是RabbitMQ中的一種特殊隊列,它可以在消息到達隊列後延遲一段時間再被消費。 > > 延時隊列的實現原理是通過使用消息的過期時間和死信隊列來實現。當消息被髮送到延時隊列時,可以為消息設置一個過期時間,這個過期時間決定了消 ...
  • ASP.NET 團隊和社區在 .NET 8 繼續全力投入 Blazor,為它帶來了非常多的新特性,特別是在服務端渲染(SSR)方面,一定程度解決之前 WASM 載入慢,Server 性能不理想等局限性,也跟原來的 MVC,Razor Pages 框架在底層完成了統一。 AntDesign Blazo ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...