您好,登錄后才能下訂單哦!
這篇“C#處理類型和二進(jìn)制數(shù)據(jù)轉(zhuǎn)換并提高程序性能的方法”文章的知識點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“C#處理類型和二進(jìn)制數(shù)據(jù)轉(zhuǎn)換并提高程序性能的方法”文章吧。
按照內(nèi)存分配來區(qū)分,C# 有值類型、引用類型;
按照基礎(chǔ)類型類型來分,C# 有 內(nèi)置類型、通用類型、自定義類型、匿名類型、元組類型、CTS類型(通用類型系統(tǒng));
C# 的基礎(chǔ)類型包括:
整型: sbyte, byte, short, ushort, int, uint, long, ulong
實(shí)數(shù)類型: float, double, decimal
字符類型: char
布爾類型: bool
字符串類型: string
C# 中的原語類型,是基礎(chǔ)類型中的值類型,不包括 string。原語類型可以使用 sizeof()
來獲取字節(jié)大小,除 bool 外,都有 MaxValue
、MinValue
兩個(gè)字段。
sizeof(uint); uint.MaxValue uint.MinValue
我們也可以在泛型上進(jìn)行區(qū)分,上面的教程類型,除了 string,其他類型都是 struct。
<T>() where T : struct { }
Buffer 可以操作基元類型(int、byte等)的數(shù)組,利用.NET 中的 Buffer 類,通過更快地訪問內(nèi)存中的數(shù)據(jù)來提高應(yīng)用程序的性能。
Buffer 可以直接從基元類型的數(shù)組中,直接取出指定數(shù)量的字節(jié),或者給其某個(gè)字節(jié)設(shè)置值。
Buffer 主要在直接操作內(nèi)存數(shù)據(jù)、操作非托管內(nèi)存時(shí),使用 Buffer 可以帶來安全且高性能的體驗(yàn)。
方法 | 說明 |
---|---|
BlockCopy(Array, Int32, Array, Int32, Int32) | 將指定數(shù)目的字節(jié)從起始于特定偏移量的源數(shù)組復(fù)制到起始于特定偏移量的目標(biāo)數(shù)組。 |
ByteLength(Array) | 返回指定數(shù)組中的字節(jié)數(shù)。 |
GetByte(Array, Int32) | 檢索指定數(shù)組中指定位置的字節(jié)。 |
MemoryCopy(Void, Void, Int64, Int64) | 將指定為長整型值的一些字節(jié)從內(nèi)存中的一個(gè)地址復(fù)制到另一個(gè)地址。此 API 不符合 CLS。 |
MemoryCopy(Void, Void, UInt64, UInt64) | 將指定為無符號長整型值的一些字節(jié)從內(nèi)存中的一個(gè)地址復(fù)制到另一個(gè)地址。此 API 不符合 CLS。 |
SetByte(Array, Int32, Byte) | 將指定的值分配給指定數(shù)組中特定位置處的字節(jié)。 |
下面來介紹一下 Buffer 的一些使用方法。
BlockCopy 可以復(fù)制數(shù)組的一部分到另一個(gè)數(shù)組,其使用方法如下:
int[] arr1 = new int[] { 1, 2, 3, 4, 5 }; int[] arr2 = new int[10] { 0, 0, 0, 0, 0, 6, 7, 8, 9, 10 }; // int = 4 byte // index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ... ... // arr1: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 // arr2: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 // Buffer.ByteLength(arr1) == 20 , // Buffer.ByteLength(arr2) == 40 Buffer.BlockCopy(arr1, 0, arr2, 0, 19); for (int i = 0; i < arr2.Length; i++) { Console.Write(arr2[i] + ","); }
.SetByte()
則可細(xì)粒度地設(shè)置數(shù)組的值,即可以直接設(shè)置數(shù)組中任意一位的值,其使用方法如下:
//source data: // 0000,0001,0002,00003,0004 // 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 int[] a = new int[] { 0, 1, 2, 3, 4 }; foreach (var item in a) { Console.Write(item + ","); } Console.WriteLine("\n------\n"); // see : https://stackoverflow.com/questions/26455843/how-are-array-values-stored-in-little-endian-vs-big-endian-architecture // memory save that data: // 0000 1000 2000 3000 4000 for (int i = 0; i < Buffer.ByteLength(a); i++) { Console.Write(Buffer.GetByte(a, i)); if (i != 0 && (i + 1) % 4 == 0) Console.Write(" "); } // 16 進(jìn)制 // 0000 1000 2000 3000 4000 Console.WriteLine("\n------\n"); Buffer.SetByte(a, 0, 4); Buffer.SetByte(a, 4, 3); Buffer.SetByte(a, 8, 2); Buffer.SetByte(a, 12, 1); Buffer.SetByte(a, 16, 0); foreach (var item in a) { Console.Write(item + ","); } Console.WriteLine("\n------\n");
建議自行測試,斷點(diǎn)調(diào)試,觀察過程。
System.Buffers.Binary.BinaryPrimitives
用來以精確的方式讀取或者字節(jié)數(shù)組,只能對 byte
或 byte
數(shù)組使用,其使用場景非常廣泛。
BinaryPrimitives 的實(shí)現(xiàn)原理是 BitConverter
,BinaryPrimitives 對 BitConverter 做了一些封裝。BinaryPrimitives 的主要使用方式是以某種形式從 byte 或 byte 數(shù)組中讀取出信息。
例如,BinaryPrimitives 在 byte 數(shù)組中,一次性讀取四個(gè)字節(jié),其示例代碼如下:
// source data: 00 01 02 03 04 // binary data: 00000000 00000001 00000010 00000011 000001000 byte[] arr = new byte[] { 0, 1, 2, 3, 4, }; // read one int,4 byte int head = BinaryPrimitives.ReadInt32BigEndian(arr); // 5 byte: 00000000 00000001 00000010 00000011 000001000 // read 4 byte(int) : 00000000 00000001 00000010 00000011 // = 66051 Console.WriteLine(head);
在 BinaryPrimitives 中有大端小端之分。在 C# 中,應(yīng)該都是小端在前大端在后的,具體可能會因處理器架構(gòu)而不同。
你可以使用BitConverter.IsLittleEndian
來判斷在當(dāng)前處理器上,C# 程序是大端還是小端在前。
以 .Read...()
開頭的方法,可以以字節(jié)為定位訪問 byte
數(shù)組上的數(shù)據(jù)。
以 .Write...()
開頭的方法,可以向某個(gè)位置寫入數(shù)據(jù)。
下面舉個(gè)例子:
// source data: 00 01 02 03 04 // binary data: 00000000 00000001 00000010 00000011 000001000 byte[] arr = new byte[] { 0, 1, 2, 3, 4, }; // read one int,4 byte // 5 byte: 00000000 00000001 00000010 00000011 000001000 // read 4 byte(int) : 00000000 00000001 00000010 00000011 // = 66051 int head = BinaryPrimitives.ReadInt32BigEndian(arr); Console.WriteLine(head); // BinaryPrimitives.WriteInt32LittleEndian(arr, 1); BinaryPrimitives.WriteInt32BigEndian(arr.AsSpan().Slice(0, 4), 0b00000000_00000000_00000000_00000001); // to : 00000000 00000000 00000000 00000001 | 000001000 // read 4 byte head = BinaryPrimitives.ReadInt32BigEndian(arr); Console.WriteLine(head);
建議自行測試,斷點(diǎn)調(diào)試,觀察過程。
C#和.NET Core 有的許多面向性能的 API,C# 和 .NET 的一大優(yōu)點(diǎn)是可以在不犧牲內(nèi)存安全性的情況下編寫快速出高性能的庫。我們在避免使用 unsafe 代碼的情況下,通過二進(jìn)制處理類,我們可以編寫出高性能的代碼和具有安全性的代碼。
在 C# 中,我們有以下類型可以高效操作字節(jié)/內(nèi)存:
Span
和C#類型可以快速安全地訪問內(nèi)存。表示任意內(nèi)存的連續(xù)區(qū)域。使用 span 使我們可以序列化為托管.NET數(shù)組,堆棧分配的數(shù)組或非托管內(nèi)存,而無需使用指針。.NET可以防止緩沖區(qū)溢出。
ref struct
、 Span
stackalloc
用于創(chuàng)建基于堆棧的數(shù)組。stackalloc
是在需要較小緩沖區(qū)時(shí)避免分配的有用工具。
低級方法,并在原始類型和字節(jié)之間直接轉(zhuǎn)換。MemoryMarshal.GetReference()
、Unsafe.ReadUnaligned()
、Unsafe.WriteUnaligned()
BinaryPrimitives
具有用于在.NET基本類型和字節(jié)之間進(jìn)行有效轉(zhuǎn)換的輔助方法。例如,讀取小尾數(shù)字節(jié)并返回?zé)o符號的64位數(shù)字。所提供的方法經(jīng)過了最優(yōu)化,并使用了向量化。BinaryPrimitives.ReadUInt64LittleEndian
、BinaryPrimitive
以 .Reverse...()
開頭的方法,可以置換基元類型的大小端。
short value = 0b00000000_00000001; // to endianness: 0b00000001_00000000 == 256 BinaryPrimitives.ReverseEndianness(0b00000000_00000000_00000000_00000001); Console.WriteLine(BinaryPrimitives.ReverseEndianness(value)); value = 0b00000001_00000000; Console.WriteLine(BinaryPrimitives.ReverseEndianness(value)); // 1
BitConverter 可以基元類型和 byte 相互轉(zhuǎn)換,例如 int 和 byte 互轉(zhuǎn),或者任意取出、寫入基元類型的任意一個(gè)字節(jié)。
其示例如下:
// 0b...1_00000100 int value = 260; // byte max value:255 // a = 0b00000100; 丟失 int ... 00000100 之前的位數(shù)。 byte a = (byte)value; // a = 4 Console.WriteLine(a); // LittleEndian // 0b 00000100 00000001 00000000 00000000 byte[] b = BitConverter.GetBytes(260); Console.WriteLine(Buffer.GetByte(b, 1)); // 4 if (BitConverter.IsLittleEndian) Console.WriteLine(BinaryPrimitives.ReadInt32LittleEndian(b)); else Console.WriteLine(BinaryPrimitives.ReadInt32BigEndian(b));
MemoryMarshal 提供與 Memory<T>
、ReadOnlyMemory<T>
、Span<T>
和 ReadOnlySpan<T>
進(jìn)行交互操作的方法。
MemoryMarshal
在 System.Runtime.InteropServices
命名空間中。
我們先介紹 MemoryMarshal.Cast()
,它可以將一種基元類型的范圍強(qiáng)制轉(zhuǎn)換為另一種基元類型的范圍。
// 1 int = 4 byte // int [] {1,2} // 0001 0002 var byteArray = new byte[] { 1, 0, 0, 0, 2, 0, 0, 0 }; Span<byte> byteSpan = byteArray.AsSpan(); // byte to int Span<int> intSpan = MemoryMarshal.Cast<byte, int>(byteSpan); foreach (var item in intSpan) { Console.Write(item + ","); }
最簡單的說法是,MemoryMarshal 可以將一種結(jié)構(gòu)轉(zhuǎn)換為另一種結(jié)構(gòu)。
我們可以將一個(gè)結(jié)構(gòu)轉(zhuǎn)換為字節(jié):
public struct Test { public int A; public int B; public int C; } ... ... Test test = new Test() { A = 1, B = 2, C = 3 }; var testArray = new Test[] { test }; ReadOnlySpan<byte> tmp = MemoryMarshal.AsBytes(testArray.AsSpan()); // socket.Send(tmp); ...
還可以逆向還原字節(jié)為結(jié)構(gòu)體:
// bytes = socket.Accept(); .. ReadOnlySpan<Test> testSpan = MemoryMarshal.Cast<byte,Test>(tmp); // or Test testSpan = MemoryMarshal.Read<Test>(tmp);
例如,我們要對比兩個(gè)結(jié)構(gòu)體數(shù)組中,每個(gè)結(jié)構(gòu)體是否相等,可以采用以下代碼:
static void Main(string[] args) { int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int[] b = new int[] { 1, 2, 3, 4, 5, 6, 7, 0, 9 }; _ = Compare64(a,b); } private static bool Compare64<T>(T[] t1, T[] t2) where T : struct { var l1 = MemoryMarshal.Cast<T, long>(t1); var l2 = MemoryMarshal.Cast<T, long>(t2); for (int i = 0; i < l1.Length; i++) { if (l1[i] != l2[i]) return false; } return true; }
后面有個(gè)更好的性能提升方案。
程序員基本都學(xué)習(xí)過 C 語言,應(yīng)該了解 C 語言中的結(jié)構(gòu)體字節(jié)對齊,在 C# 中也是一樣,兩種類型相互轉(zhuǎn)換,除了 C# 結(jié)構(gòu)體轉(zhuǎn) C# 結(jié)構(gòu)體,也可以 C 語言結(jié)構(gòu)體轉(zhuǎn) C# 結(jié)構(gòu)體,但是要考慮好字節(jié)對齊,如果兩個(gè)結(jié)構(gòu)體所占用的內(nèi)存大小不一樣,則可能在轉(zhuǎn)換時(shí)出現(xiàn)數(shù)據(jù)丟失或出現(xiàn)錯(cuò)誤。
Marshal 提供了用于分配非托管內(nèi)存,復(fù)制非托管內(nèi)存塊以及將托管類型轉(zhuǎn)換為非托管類型的方法的集合,以及與非托管代碼進(jìn)行交互時(shí)使用的其他方法,或者用來確定對象的大小。
例如,來確定 C# 中的一些類型大?。?/p>
Console.WriteLine("SystemDefaultCharSize={0}, SystemMaxDBCSCharSize={1}", Marshal.SystemDefaultCharSize, Marshal.SystemMaxDBCSCharSize);
輸出 char 占用的字節(jié)數(shù)。
例如,在調(diào)用非托管代碼時(shí),需要傳遞函數(shù)指針,C# 一般使用委托傳遞,很多時(shí)候?yàn)榱吮苊飧鞣N內(nèi)存問題異常問題,需要轉(zhuǎn)換為指針傳遞。
IntPtr p = Marshal.GetFunctionPointerForDelegate(_overrideCompileMethod)
Marshal 也可以很方便地獲得一個(gè)結(jié)構(gòu)體的字節(jié)大?。?/p>
public struct Point { public Int32 x, y; } Marshal.SizeOf(typeof(Point));
從非托管內(nèi)存中分配一塊內(nèi)存和釋放內(nèi)存,我們可以避免 usafe 代碼的使用,代碼示例:
IntPtr hglobal = Marshal.AllocHGlobal(100); Marshal.FreeHGlobal(hglobal);
合理利用前面提到的二進(jìn)制處理類,可以在很多方面提升代碼性能,在前面的學(xué)習(xí)中,我們大概了解這些對象,但是有什么應(yīng)用場景?真的能夠提升性能?有沒有練習(xí)代碼?
這里筆者舉個(gè)例子,如何比較兩個(gè) byte[] 數(shù)組是否相等?
最簡單的代碼示例如下:
public bool ForBytes(byte[] a,byte[] b) { if (a.Length != b.Length) return false; for (int i = 0; i < a.Length; i++) { if (a[i] != b[i]) return false; } return true; }
這個(gè)代碼很簡單,循環(huán)遍歷字節(jié)數(shù)組,一個(gè)個(gè)判斷是否相等。
如果用上前面的二進(jìn)制處理對象類,則可以這樣寫代碼:
private static bool EqualsBytes(byte[] b1, byte[] b2) { var a = b1.AsSpan(); var b = b2.AsSpan(); Span<byte> copy1 = default; Span<byte> copy2 = default; if (a.Length != b.Length) return false; for (int i = 0; i < a.Length;) { if (a.Length - 8 > i) { copy1 = a.Slice(i, 8); copy2 = b.Slice(i, 8); if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) return false; i += 8; continue; } if (a[i] != b[i]) return false; i++; } return true; }
你可能會在想,第二種方法,這么多代碼,這么多判斷,還有各種函數(shù)調(diào)用,還多創(chuàng)建了一些對象,這特么能夠提升速度?這樣會不會消耗更多內(nèi)存??? 別急,你可以使用以下完整代碼測試:
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; using System; using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Text; namespace BenTest { [SimpleJob(RuntimeMoniker.NetCoreApp31)] [SimpleJob(RuntimeMoniker.CoreRt31)] [RPlotExporter] public class Test { private byte[] _a = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666"); private byte[] _b = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666"); private int[] A1 = new int[] { 41544444, 4487, 841, 8787, 4415, 7, 458, 4897, 87897, 815, 485, 4848, 787, 41, 5489, 74878, 84, 89787, 8456, 4857489, 784, 85489, 47 }; private int[] B2 = new int[] { 41544444, 4487, 841, 8787, 4415, 7, 458, 4897, 87897, 815, 485, 4848, 787, 41, 5489, 74878, 84, 89787, 8456, 4857489, 784, 85489, 47 }; [Benchmark] public bool ForBytes() { for (int i = 0; i < _a.Length; i++) { if (_a[i] != _b[i]) return false; } return true; } [Benchmark] public bool ForArray() { return ForArray(A1, B2); } private bool ForArray<T>(T[] b1, T[] b2) where T : struct { for (int i = 0; i < b1.Length; i++) { if (!b1[i].Equals(b2[i])) return false; } return true; } [Benchmark] public bool EqualsArray() { return EqualArray(A1, B2); } [Benchmark] public bool EqualsBytes() { var a = _a.AsSpan(); var b = _b.AsSpan(); Span<byte> copy1 = default; Span<byte> copy2 = default; if (a.Length != b.Length) return false; for (int i = 0; i < a.Length;) { if (a.Length - 8 > i) { copy1 = a.Slice(i, 8); copy2 = b.Slice(i, 8); if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) return false; i += 8; continue; } if (a[i] != b[i]) return false; i++; } return true; } private bool EqualArray<T>(T[] t1, T[] t2) where T : struct { Span<byte> b1 = MemoryMarshal.AsBytes<T>(t1.AsSpan()); Span<byte> b2 = MemoryMarshal.AsBytes<T>(t2.AsSpan()); Span<byte> copy1 = default; Span<byte> copy2 = default; if (b1.Length != b2.Length) return false; for (int i = 0; i < b1.Length;) { if (b1.Length - 8 > i) { copy1 = b1.Slice(i, 8); copy2 = b2.Slice(i, 8); if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) return false; i += 8; continue; } if (b1[i] != b2[i]) return false; i++; } return true; } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<Test>(); Console.ReadKey(); } } }
使用 BenchmarkDotNet 的測試結(jié)果如下:
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1052 (21H1/May2021Update) Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores .NET SDK=5.0.301 [Host] : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT .NET Core 3.1 : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT | Method | Job | Runtime | Mean | Error | StdDev | |------------ |-------------- |-------------- |---------:|---------:|---------:| | ForBytes | .NET Core 3.1 | .NET Core 3.1 | 76.95 ns | 0.064 ns | 0.053 ns | | ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.37 ns | 1.258 ns | 1.177 ns | | EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.91 ns | 0.027 ns | 0.024 ns | | EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 26.26 ns | 0.432 ns | 0.383 ns |
可以看到,byte[] 比較中,使用了二進(jìn)制對象的方式,耗時(shí)下降了近 60ns,而在 struct 的比較中,耗時(shí)也下降了 40ns。
在第二種代碼中,我們使用了 Span、切片、 MemoryMarshal、BinaryPrimitives,這些用法都可以給我們的程序性能帶來很大的提升。
這里示例雖然使用了 Span 等,其最主要是利用了 64位 CPU ,64位 CPU 能夠一次性讀取 8個(gè)字節(jié)(64位),因此我們使用 ReadUInt64BigEndian
一次讀取從字節(jié)數(shù)組中讀取 8 個(gè)字節(jié)去進(jìn)行比較。如果字節(jié)數(shù)組長度為 1024 ,那么第二種方法只需要 比較 128次。
當(dāng)然,這里并不是這種代碼性能是最強(qiáng)的,因?yàn)?CLR 有很多底層方法具有更猛的性能。不過,我們也看到了,合理使用這些類型,能夠很大程度上提高代碼性能。上面的數(shù)組對比只是一個(gè)簡單的例子,在實(shí)際項(xiàng)目中,我們也可以挖掘更多使用場景。
雖然第二種方法,快了幾倍,但是性能還不夠強(qiáng)勁,我們可以利用 Span 中的 API,來實(shí)現(xiàn)更快的比較。
[Benchmark] public bool SpanEqual() { return SpanEqual(_a,_b); } private bool SpanEqual(byte[] a, byte[] b) { return a.AsSpan().SequenceEqual(b); }
可以試試
StructuralComparisons.StructuralEqualityComparer.Equals(a, b);
性能測試結(jié)果:
| Method | Job | Runtime | Mean | Error | StdDev | |------------ |-------------- |-------------- |----------:|----------:|----------:| | ForBytes | .NET Core 3.1 | .NET Core 3.1 | 77.025 ns | 0.0502 ns | 0.0419 ns | | ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.192 ns | 0.6127 ns | 0.5117 ns | | EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.897 ns | 0.0122 ns | 0.0108 ns | | EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 25.722 ns | 0.4584 ns | 0.4287 ns | | SpanEqual | .NET Core 3.1 | .NET Core 3.1 | 4.736 ns | 0.0099 ns | 0.0093 ns |
可以看到,Span.SequenceEqual()
的速度簡直是碾壓。
以上就是關(guān)于“C#處理類型和二進(jìn)制數(shù)據(jù)轉(zhuǎn)換并提高程序性能的方法”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。