C#处理类型和二进制数据转换并提高程序性能的方法

C#处理类型和二进制数据转换并提高程序性能的方法

这篇“C#处理类型和二进制数据转换并提高程序性能的方法”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“C#处理类型和二进制数据转换并提高程序性能的方法”文章吧。

    C# 原语类型

    按照内存分配来区分,C# 有值类型、引用类型;

    C#处理类型和二进制数据转换并提高程序性能的方法

    按照基础类型类型来分,C# 有 内置类型、通用类型、自定义类型、匿名类型、元组类型、CTS类型(通用类型系统);

    C# 的基础类型包括:

    • 整型: sbyte, byte, short, ushort, int, uint, long, ulong

    • 实数类型: float, double, decimal

    • 字符类型: char

    • 布尔类型: bool

    • 字符串类型: string

    C# 中的原语类型,是基础类型中的值类型,不包括 string。原语类型可以使用sizeof()来获取字节大小,除 bool 外,都有MaxValueMinValue两个字段。

    sizeof(uint);uint.MaxValueuint.MinValue

    我们也可以在泛型上进行区分,上面的教程类型,除了 string,其他类型都是 struct。

    <T>()whereT:struct{}

    1,利用 Buffer 优化数组性能

    Buffer 可以操作基元类型(int、byte等)的数组,利用.NET 中的 Buffer 类,通过更快地访问内存中的数据来提高应用程序的性能。
    Buffer 可以直接从基元类型的数组中,直接取出指定数量的字节,或者给其某个字节设置值。

    Buffer 主要在直接操作内存数据、操作非托管内存时,使用 Buffer 可以带来安全且高性能的体验。

    方法说明
    BlockCopy(Array, Int32, Array, Int32, Int32)将指定数目的字节从起始于特定偏移量的源数组复制到起始于特定偏移量的目标数组。
    ByteLength(Array)返回指定数组中的字节数。
    GetByte(Array, Int32)检索指定数组中指定位置的字节。
    MemoryCopy(Void, Void, Int64, Int64)将指定为长整型值的一些字节从内存中的一个地址复制到另一个地址。此 API 不符合 CLS。
    MemoryCopy(Void, Void, UInt64, UInt64)将指定为无符号长整型值的一些字节从内存中的一个地址复制到另一个地址。此 API 不符合 CLS。
    SetByte(Array, Int32, Byte)将指定的值分配给指定数组中特定位置处的字节。

    下面来介绍一下 Buffer 的一些使用方法。

    BlockCopy 可以复制数组的一部分到另一个数组,其使用方法如下:

    int[]arr1=newint[]{1,2,3,4,5};int[]arr2=newint[10]{0,0,0,0,0,6,7,8,9,10};//int=4byte//index:012345678910111213141516171819......//arr1:0100000002000000030000000400000005000000//arr2:0000000000000000000000000000000000000000060000000700000008000000090000000A000000//Buffer.ByteLength(arr1)==20,//Buffer.ByteLength(arr2)==40Buffer.BlockCopy(arr1,0,arr2,0,19);for(inti=0;i<arr2.Length;i++){Console.Write(arr2[i]+",");}

    .SetByte()则可细粒度地设置数组的值,即可以直接设置数组中任意一位的值,其使用方法如下:

    //sourcedata://0000,0001,0002,00003,0004//0000000001000000020000000300000004000000int[]a=newint[]{0,1,2,3,4};foreach(varitemina){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//memorysavethatdata://00001000200030004000for(inti=0;i<Buffer.ByteLength(a);i++){Console.Write(Buffer.GetByte(a,i));if(i!=0&&(i+1)%4==0)Console.Write("");}//16进制//00001000200030004000Console.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(varitemina){Console.Write(item+",");}Console.WriteLine("\n------\n");

    建议自行测试,断点调试,观察过程。

    2,BinaryPrimitives 细粒度操作字节数组

    System.Buffers.Binary.BinaryPrimitives用来以精确的方式读取或者字节数组,只能对bytebyte数组使用,其使用场景非常广泛。

    BinaryPrimitives 的实现原理是BitConverter,BinaryPrimitives 对 BitConverter 做了一些封装。BinaryPrimitives 的主要使用方式是以某种形式从 byte 或 byte 数组中读取出信息。

    例如,BinaryPrimitives 在 byte 数组中,一次性读取四个字节,其示例代码如下:

    //sourcedata:0001020304//binarydata:00000000000000010000001000000011000001000byte[]arr=newbyte[]{0,1,2,3,4,};//readoneint,4byteinthead=BinaryPrimitives.ReadInt32BigEndian(arr);//5byte:00000000000000010000001000000011000001000//read4byte(int):00000000000000010000001000000011//=66051Console.WriteLine(head);

    在 BinaryPrimitives 中有大端小端之分。在 C# 中,应该都是小端在前大端在后的,具体可能会因处理器架构而不同。
    你可以使用BitConverter.IsLittleEndian来判断在当前处理器上,C# 程序是大端还是小端在前。

    .Read...()开头的方法,可以以字节为定位访问byte数组上的数据。

    .Write...()开头的方法,可以向某个位置写入数据。

    下面举个例子:

    //sourcedata:0001020304//binarydata:00000000000000010000001000000011000001000byte[]arr=newbyte[]{0,1,2,3,4,};//readoneint,4byte//5byte:00000000000000010000001000000011000001000//read4byte(int):00000000000000010000001000000011//=66051inthead=BinaryPrimitives.ReadInt32BigEndian(arr);Console.WriteLine(head);//BinaryPrimitives.WriteInt32LittleEndian(arr,1);BinaryPrimitives.WriteInt32BigEndian(arr.AsSpan().Slice(0,4),0b00000000_00000000_00000000_00000001);//to:00000000000000000000000000000001|000001000//read4bytehead=BinaryPrimitives.ReadInt32BigEndian(arr);Console.WriteLine(head);

    建议自行测试,断点调试,观察过程。

    提高代码安全性

    C#和.NET Core 有的许多面向性能的 API,C# 和 .NET 的一大优点是可以在不牺牲内存安全性的情况下编写快速出高性能的库。我们在避免使用 unsafe 代码的情况下,通过二进制处理类,我们可以编写出高性能的代码和具有安全性的代码。

    在 C# 中,我们有以下类型可以高效操作字节/内存:

    • Span和C#类型可以快速安全地访问内存。表示任意内存的连续区域。使用 span 使我们可以序列化为托管.NET数组,堆栈分配的数组或非托管内存,而无需使用指针。.NET可以防止缓冲区溢出。

    • ref structSpan

    • stackalloc用于创建基于堆栈的数组。stackalloc是在需要较小缓冲区时避免分配的有用工具。

    • 低级方法,并在原始类型和字节之间直接转换。MemoryMarshal.GetReference()Unsafe.ReadUnaligned()Unsafe.WriteUnaligned()

    • BinaryPrimitives具有用于在.NET基本类型和字节之间进行有效转换的辅助方法。例如,读取小尾数字节并返回无符号的64位数字。所提供的方法经过了最优化,并使用了向量化。BinaryPrimitives.ReadUInt64LittleEndianBinaryPrimitive

    .Reverse...()开头的方法,可以置换基元类型的大小端。

    shortvalue=0b00000000_00000001;//toendianness:0b00000001_00000000==256BinaryPrimitives.ReverseEndianness(0b00000000_00000000_00000000_00000001);Console.WriteLine(BinaryPrimitives.ReverseEndianness(value));value=0b00000001_00000000;Console.WriteLine(BinaryPrimitives.ReverseEndianness(value));//1

    3,BitConverter、MemoryMarshal

    BitConverter 可以基元类型和 byte 相互转换,例如 int 和 byte 互转,或者任意取出、写入基元类型的任意一个字节。
    其示例如下:

    //0b...1_00000100intvalue=260;//bytemaxvalue:255//a=0b00000100;丢失int...00000100之前的位数。bytea=(byte)value;//a=4Console.WriteLine(a);//LittleEndian//0b00000100000000010000000000000000byte[]b=BitConverter.GetBytes(260);Console.WriteLine(Buffer.GetByte(b,1));//4if(BitConverter.IsLittleEndian)Console.WriteLine(BinaryPrimitives.ReadInt32LittleEndian(b));elseConsole.WriteLine(BinaryPrimitives.ReadInt32BigEndian(b));

    MemoryMarshal 提供与Memory<T>ReadOnlyMemory<T>Span<T>ReadOnlySpan<T>进行交互操作的方法。

    MemoryMarshalSystem.Runtime.InteropServices命名空间中。

    我们先介绍MemoryMarshal.Cast(),它可以将一种基元类型的范围强制转换为另一种基元类型的范围。

    //1int=4byte//int[]{1,2}//00010002varbyteArray=newbyte[]{1,0,0,0,2,0,0,0};Span<byte>byteSpan=byteArray.AsSpan();//bytetointSpan<int>intSpan=MemoryMarshal.Cast<byte,int>(byteSpan);foreach(variteminintSpan){Console.Write(item+",");}

    最简单的说法是,MemoryMarshal可以将一种结构转换为另一种结构。

    我们可以将一个结构转换为字节:

    publicstructTest{publicintA;publicintB;publicintC;}......Testtest=newTest(){A=1,B=2,C=3};vartestArray=newTest[]{test};ReadOnlySpan<byte>tmp=MemoryMarshal.AsBytes(testArray.AsSpan());//socket.Send(tmp);...

    还可以逆向还原字节为结构体:

    //bytes=socket.Accept();..ReadOnlySpan<Test>testSpan=MemoryMarshal.Cast<byte,Test>(tmp);//orTesttestSpan=MemoryMarshal.Read<Test>(tmp);

    例如,我们要对比两个结构体数组中,每个结构体是否相等,可以采用以下代码:

    staticvoidMain(string[]args){int[]a=newint[]{1,2,3,4,5,6,7,8,9};int[]b=newint[]{1,2,3,4,5,6,7,0,9};_=Compare64(a,b);}privatestaticboolCompare64<T>(T[]t1,T[]t2)whereT:struct{varl1=MemoryMarshal.Cast<T,long>(t1);varl2=MemoryMarshal.Cast<T,long>(t2);for(inti=0;i<l1.Length;i++){if(l1[i]!=l2[i])returnfalse;}returntrue;}

    后面有个更好的性能提升方案。

    程序员基本都学习过 C 语言,应该了解 C 语言中的结构体字节对齐,在 C# 中也是一样,两种类型相互转换,除了 C# 结构体转 C# 结构体,也可以 C 语言结构体转 C# 结构体,但是要考虑好字节对齐,如果两个结构体所占用的内存大小不一样,则可能在转换时出现数据丢失或出现错误。

    4,Marshal

    Marshal 提供了用于分配非托管内存,复制非托管内存块以及将托管类型转换为非托管类型的方法的集合,以及与非托管代码进行交互时使用的其他方法,或者用来确定对象的大小。

    例如,来确定 C# 中的一些类型大小:

    Console.WriteLine("SystemDefaultCharSize={0},SystemMaxDBCSCharSize={1}",Marshal.SystemDefaultCharSize,Marshal.SystemMaxDBCSCharSize);

    输出 char 占用的字节数。

    例如,在调用非托管代码时,需要传递函数指针,C# 一般使用委托传递,很多时候为了避免各种内存问题异常问题,需要转换为指针传递。

    IntPtrp=Marshal.GetFunctionPointerForDelegate(_overrideCompileMethod)

    Marshal 也可以很方便地获得一个结构体的字节大小:

    publicstructPoint{publicInt32x,y;}Marshal.SizeOf(typeof(Point));

    从非托管内存中分配一块内存和释放内存,我们可以避免 usafe 代码的使用,代码示例:

    IntPtrhglobal=Marshal.AllocHGlobal(100);Marshal.FreeHGlobal(hglobal);

    实践

    合理利用前面提到的二进制处理类,可以在很多方面提升代码性能,在前面的学习中,我们大概了解这些对象,但是有什么应用场景?真的能够提升性能?有没有练习代码?

    这里笔者举个例子,如何比较两个 byte[] 数组是否相等?
    最简单的代码示例如下:

    publicboolForBytes(byte[]a,byte[]b){if(a.Length!=b.Length)returnfalse;for(inti=0;i<a.Length;i++){if(a[i]!=b[i])returnfalse;}returntrue;}

    这个代码很简单,循环遍历字节数组,一个个判断是否相等。

    如果用上前面的二进制处理对象类,则可以这样写代码:

    privatestaticboolEqualsBytes(byte[]b1,byte[]b2){vara=b1.AsSpan();varb=b2.AsSpan();Span<byte>copy1=default;Span<byte>copy2=default;if(a.Length!=b.Length)returnfalse;for(inti=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))returnfalse;i+=8;continue;}if(a[i]!=b[i])returnfalse;i++;}returntrue;}

    你可能会在想,第二种方法,这么多代码,这么多判断,还有各种函数调用,还多创建了一些对象,这特么能够提升速度?这样会不会消耗更多内存??? 别急,你可以使用以下完整代码测试:

    usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Jobs;usingBenchmarkDotNet.Running;usingSystem;usingSystem.Buffers.Binary;usingSystem.Runtime.InteropServices;usingSystem.Text;namespaceBenTest{[SimpleJob(RuntimeMoniker.NetCoreApp31)][SimpleJob(RuntimeMoniker.CoreRt31)][RPlotExporter]publicclassTest{privatebyte[]_a=Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666");privatebyte[]_b=Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666");privateint[]A1=newint[]{41544444,4487,841,8787,4415,7,458,4897,87897,815,485,4848,787,41,5489,74878,84,89787,8456,4857489,784,85489,47};privateint[]B2=newint[]{41544444,4487,841,8787,4415,7,458,4897,87897,815,485,4848,787,41,5489,74878,84,89787,8456,4857489,784,85489,47};[Benchmark]publicboolForBytes(){for(inti=0;i<_a.Length;i++){if(_a[i]!=_b[i])returnfalse;}returntrue;}[Benchmark]publicboolForArray(){returnForArray(A1,B2);}privateboolForArray<T>(T[]b1,T[]b2)whereT:struct{for(inti=0;i<b1.Length;i++){if(!b1[i].Equals(b2[i]))returnfalse;}returntrue;}[Benchmark]publicboolEqualsArray(){returnEqualArray(A1,B2);}[Benchmark]publicboolEqualsBytes(){vara=_a.AsSpan();varb=_b.AsSpan();Span<byte>copy1=default;Span<byte>copy2=default;if(a.Length!=b.Length)returnfalse;for(inti=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))returnfalse;i+=8;continue;}if(a[i]!=b[i])returnfalse;i++;}returntrue;}privateboolEqualArray<T>(T[]t1,T[]t2)whereT: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)returnfalse;for(inti=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))returnfalse;i+=8;continue;}if(b1[i]!=b2[i])returnfalse;i++;}returntrue;}}classProgram{staticvoidMain(string[]args){varsummary=BenchmarkRunner.Run<Test>();Console.ReadKey();}}}

    使用 BenchmarkDotNet 的测试结果如下:

    BenchmarkDotNet=v0.13.0,OS=Windows10.0.19043.1052(21H1/May2021Update)IntelCorei7-10700CPU2.90GHz,1CPU,16logicaland8physicalcores.NETSDK=5.0.301[Host]:.NETCore3.1.16(CoreCLR4.700.21.26205,CoreFX4.700.21.26205),X64RyuJIT.NETCore3.1:.NETCore3.1.16(CoreCLR4.700.21.26205,CoreFX4.700.21.26205),X64RyuJIT|Method|Job|Runtime|Mean|Error|StdDev||------------|--------------|--------------|---------:|---------:|---------:||ForBytes|.NETCore3.1|.NETCore3.1|76.95ns|0.064ns|0.053ns||ForArray|.NETCore3.1|.NETCore3.1|66.37ns|1.258ns|1.177ns||EqualsArray|.NETCore3.1|.NETCore3.1|17.91ns|0.027ns|0.024ns||EqualsBytes|.NETCore3.1|.NETCore3.1|26.26ns|0.432ns|0.383ns|

    可以看到,byte[] 比较中,使用了二进制对象的方式,耗时下降了近 60ns,而在 struct 的比较中,耗时也下降了 40ns。

    在第二种代码中,我们使用了 Span、切片、 MemoryMarshal、BinaryPrimitives,这些用法都可以给我们的程序性能带来很大的提升。

    这里示例虽然使用了 Span 等,其最主要是利用了 64位 CPU ,64位 CPU 能够一次性读取 8个字节(64位),因此我们使用ReadUInt64BigEndian一次读取从字节数组中读取 8 个字节去进行比较。如果字节数组长度为 1024 ,那么第二种方法只需要 比较 128次。

    当然,这里并不是这种代码性能是最强的,因为 CLR 有很多底层方法具有更猛的性能。不过,我们也看到了,合理使用这些类型,能够很大程度上提高代码性能。上面的数组对比只是一个简单的例子,在实际项目中,我们也可以挖掘更多使用场景。

    更高性能

    虽然第二种方法,快了几倍,但是性能还不够强劲,我们可以利用 Span 中的 API,来实现更快的比较。

    [Benchmark]publicboolSpanEqual(){returnSpanEqual(_a,_b);}privateboolSpanEqual(byte[]a,byte[]b){returna.AsSpan().SequenceEqual(b);}

    可以试试

    StructuralComparisons.StructuralEqualityComparer.Equals(a,b);

    性能测试结果:

    |Method|Job|Runtime|Mean|Error|StdDev||------------|--------------|--------------|----------:|----------:|----------:||ForBytes|.NETCore3.1|.NETCore3.1|77.025ns|0.0502ns|0.0419ns||ForArray|.NETCore3.1|.NETCore3.1|66.192ns|0.6127ns|0.5117ns||EqualsArray|.NETCore3.1|.NETCore3.1|17.897ns|0.0122ns|0.0108ns||EqualsBytes|.NETCore3.1|.NETCore3.1|25.722ns|0.4584ns|0.4287ns||SpanEqual|.NETCore3.1|.NETCore3.1|4.736ns|0.0099ns|0.0093ns|

    可以看到,Span.SequenceEqual()的速度简直是碾压。

    以上就是关于“C#处理类型和二进制数据转换并提高程序性能的方法”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注恰卡编程网行业资讯频道。

    发布于 2022-04-03 22:38:06
    收藏
    分享
    海报
    0 条评论
    33
    上一篇:解析Spring漏洞及修复的方法 下一篇:Spring代理对象导致的获取不到原生对象注解怎么解决
    目录

      0 条评论

      本站已关闭游客评论,请登录或者注册后再评论吧~

      忘记密码?

      图形验证码