文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

学习 CLR 源码:连续内存块数据操作的性能优化

2024-12-03 01:42

关注

目录

  • 1,利用 Buffer 优化数组性能
  • 2,BinaryPrimitives 细粒度操作字节数组
  • 3,BitConverter、MemoryMarshal

本文主要介绍 C# 命名空间 System.Buffers.Binary 中的一些二进制处理类和 Span 的简单使用方法,这些二进制处理类型是上层应用处理二进制数据的基础,掌握这些类型后,我们可以很容易地处理类型和二进制数据之间的转换以及提高程序性能。

C# 原语类型

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

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

C# 的基础类型包括:

  1. 整型: sbyte, byte, short, ushort, int, uint, long, ulong
  2. 实数类型: float, double, decimal
  3. 字符类型: char
  4. 布尔类型: bool
  5. 字符串类型: string

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

  1. sizeof(uint); 
  2. uint.MaxValue 
  3. uint.MinValue 

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

  1. () where T : struct 

更多说明,可以戳这里了解: https://www.programiz.com/csharp-programming/variables-primitive-data-types

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) 将指定的值分配给指定数组中特定位置处的字节。

CLS 指公共语言标准,请参考 https://www.cnblogs.com/whuanle/p/14141213.html#5,clscompliantattribute

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

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

  1. int[] arr1 = new int[] { 12345 }; 
  2.         int[] arr2 = new int[10] { 00000678910 }; 
  3.  
  4.         // int = 4 byte 
  5.         // index:       0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16 17 18 19 ... ... 
  6.         // arr1:        01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 
  7.         // 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 
  8.  
  9.         // Buffer.ByteLength(arr1) == 20 , 
  10.         // Buffer.ByteLength(arr2) == 40 
  11.  
  12.  
  13.         Buffer.BlockCopy(arr1, 0, arr2, 019); 
  14.  
  15.         for (int i = 0; i < arr2.Length; i++) 
  16.         { 
  17.             Console.Write(arr2[i] + ","); 
  18.         } 

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

  1. //source data: 
  2.         // 0000,0001,0002,00003,0004 
  3.         // 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 
  4.         int[] a = new int[] { 01234 }; 
  5.         foreach (var item in a) 
  6.         { 
  7.             Console.Write(item + ","); 
  8.         } 
  9.  
  10.         Console.WriteLine("\n------\n"); 
  11.  
  12.         // see : https://stackoverflow.com/questions/26455843/how-are-array-values-stored-in-little-endian-vs-big-endian-architecture 
  13.         // memory save that data: 
  14.         // 0000    1000    2000    3000    4000 
  15.         for (int i = 0; i < Buffer.ByteLength(a); i++) 
  16.         { 
  17.             Console.Write(Buffer.GetByte(a, i)); 
  18.             if (i != 0 && (i + 1) % 4 == 0
  19.                 Console.Write("    "); 
  20.         } 
  21.  
  22.         // 16 进制 
  23.         // 0000    1000    2000    3000    4000 
  24.  
  25.         Console.WriteLine("\n------\n"); 
  26.  
  27.         Buffer.SetByte(a, 04); 
  28.         Buffer.SetByte(a, 43); 
  29.         Buffer.SetByte(a, 82); 
  30.         Buffer.SetByte(a, 121); 
  31.         Buffer.SetByte(a, 160); 
  32.  
  33.         foreach (var item in a) 
  34.         { 
  35.             Console.Write(item + ","); 
  36.         } 
  37.  
  38.         Console.WriteLine("\n------\n"); 

建议复制代码自行测试,断点调试,观察过程。

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

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

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

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

  1. // source data:  00 01 02 03 04 
  2.         // binary data:  00000000 00000001 00000010 00000011 000001000 
  3.         byte[] arr = new byte[] { 01234, }; 
  4.  
  5.         // read one int,4 byte 
  6.         int head = BinaryPrimitives.ReadInt32BigEndian(arr); 
  7.  
  8.  
  9.         // 5 byte:             00000000 00000001 00000010 00000011 000001000 
  10.         // read 4 byte(int) :  00000000 00000001 00000010 00000011 
  11.         //                     = 66051 
  12.  
  13.         Console.WriteLine(head); 

在 BinaryPrimitives 中有大端小端之分。在 C# 中,应该都是小端在前大端在后的,具体可能会因处理器架构而不同。

你可以使用 BitConverter.IsLittleEndian 来判断在当前处理器上,C# 程序是大端还是小端在前。

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

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

下面举个例子:

  1. // source data:  00 01 02 03 04 
  2.         // binary data:  00000000 00000001 00000010 00000011 000001000 
  3.         byte[] arr = new byte[] { 01234, }; 
  4.  
  5.         // read one int,4 byte 
  6.         // 5 byte:             00000000 00000001 00000010 00000011 000001000 
  7.         // read 4 byte(int) :  00000000 00000001 00000010 00000011 
  8.         //                     = 66051 
  9.  
  10.         int head = BinaryPrimitives.ReadInt32BigEndian(arr); 
  11.         Console.WriteLine(head); 
  12.  
  13.         // BinaryPrimitives.WriteInt32LittleEndian(arr, 1); 
  14.         BinaryPrimitives.WriteInt32BigEndian(arr.AsSpan().Slice(04), 0b00000000_00000000_00000000_00000001); 
  15.         // to : 00000000 00000000 00000000 00000001 |  000001000 
  16.         // read 4 byte 
  17.  
  18.         head = BinaryPrimitives.ReadInt32BigEndian(arr); 
  19.         Console.WriteLine(head); 

建议复制代码自行测试,断点调试,观察过程。

提高代码安全性

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

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

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

  1. short value = 0b00000000_00000001; 
  2.         // to endianness: 0b00000001_00000000 == 256 
  3.         BinaryPrimitives.ReverseEndianness(0b00000000_00000000_00000000_00000001); 
  4.  
  5.         Console.WriteLine(BinaryPrimitives.ReverseEndianness(value)); 
  6.  
  7.         value = 0b00000001_00000000; 
  8.         Console.WriteLine(BinaryPrimitives.ReverseEndianness(value)); 
  9.         // 1 

3,BitConverter、MemoryMarshal

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

其示例如下:

  1. // 0b...1_00000100 
  2.         int value = 260
  3.          
  4.         // byte max value:255 
  5.         // a = 0b00000100; 丢失 int ... 00000100 之前的位数。 
  6.         byte a = (byte)value; 
  7.  
  8.         // a = 4 
  9.         Console.WriteLine(a); 
  10.  
  11.         // LittleEndian 
  12.         // 0b 00000100 00000001 00000000 00000000 
  13.         byte[] b = BitConverter.GetBytes(260); 
  14.         Console.WriteLine(Buffer.GetByte(b, 1)); // 4 
  15.  
  16.         if (BitConverter.IsLittleEndian) 
  17.             Console.WriteLine(BinaryPrimitives.ReadInt32LittleEndian(b)); 
  18.         else 
  19.             Console.WriteLine(BinaryPrimitives.ReadInt32BigEndian(b)); 

MemoryMarshal 提供与 Memory 、 ReadOnlyMemory 、 Span 和 ReadOnlySpan 进行交互操作的方法。

MemoryMarshal 在 System.Runtime.InteropServices 命名空间中。

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

  1. // 1 int  = 4 byte 
  2.         // int [] {1,2} 
  3.         // 0001     0002 
  4.         var byteArray = new byte[] { 10002000 }; 
  5.         Span<byte> byteSpan = byteArray.AsSpan(); 
  6.         // byte to int  
  7.         Span<int> intSpan = MemoryMarshal.Cast<byteint>(byteSpan); 
  8.         foreach (var item in intSpan) 
  9.         { 
  10.             Console.Write(item + ","); 
  11.         } 

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

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

  1. public struct Test 
  2.     public int A; 
  3.     public int B; 
  4.     public int C; 
  5.  
  6. ... ... 
  7.  
  8.         Test test = new Test() 
  9.         { 
  10.             A = 1
  11.             B = 2
  12.             C = 3 
  13.         }; 
  14.         var testArray = new Test[] { test }; 
  15.         ReadOnlySpan<byte> tmp = MemoryMarshal.AsBytes(testArray.AsSpan()); 
  16.  
  17.         // socket.Send(tmp); ... 

还可以逆向还原:

  1. // bytes = socket.Accept(); ..  
  2.         ReadOnlySpan testSpan = MemoryMarshal.Cast<byte,Test>(tmp); 
  3.  
  4.         // or 
  5.         Test testSpan = MemoryMarshal.Read(tmp); 

 

  1. static void Main(string[] args) 
  2.         { 
  3.             int[] a = new int[] { 123456789 }; 
  4.             int[] b = new int[] { 123456709 }; 
  5.  
  6.  
  7.         } 
  8.  
  9.         private static bool Compare64(T[] t1, T[] t2) 
  10.             where T : struct 
  11.         { 
  12.             var l1 = MemoryMarshal.Castlong>(t1); 
  13.             var l2 = MemoryMarshal.Castlong>(t2); 
  14.  
  15.             for (int i = 0; i < l1.Length; i++) 
  16.             { 
  17.                 if (l1[i] != l2[i]) return false
  18.             } 
  19.             return true
  20.         } 

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

4,Marshal

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

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

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

输出 char 占用的字节数。

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

  1. IntPtr p = Marshal.GetFunctionPointerForDelegate(_overrideCompileMethod) 

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

  1. public struct Point 
  2.     public Int32 x, y; 
  3.  
  4. Marshal.SizeOf(typeof(Point)); 

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

  1. IntPtr hglobal = Marshal.AllocHGlobal(100); 
  2.         Marshal.FreeHGlobal(hglobal); 

实践

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

这里笔者举个例子,如何比较两个 byte[] 数组是否相等?

最简单的代码示例如下:

  1. public bool ForBytes(byte[] a,byte[] b) 
  2.         { 
  3.             if (a.Length != b.Length) 
  4.                 return false
  5.                  
  6.             for (int i = 0; i < a.Length; i++) 
  7.             { 
  8.                 if (a[i] != b[i]) return false
  9.             } 
  10.             return true
  11.         } 

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

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

  1. private static bool EqualsBytes(byte[] b1, byte[] b2) 
  2.         { 
  3.             var a = b1.AsSpan(); 
  4.             var b = b2.AsSpan(); 
  5.             Span<byte> copy1 = default
  6.             Span<byte> copy2 = default
  7.  
  8.             if (a.Length != b.Length) 
  9.                 return false
  10.  
  11.             for (int i = 0; i < a.Length;) 
  12.             { 
  13.                 if (a.Length - 8 > i) 
  14.                 { 
  15.                     copy1 = a.Slice(i, 8); 
  16.                     copy2 = b.Slice(i, 8); 
  17.                     if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) 
  18.                         return false
  19.                     i += 8
  20.                     continue
  21.                 } 
  22.  
  23.                 if (a[i] != b[i]) 
  24.                     return false
  25.                 i++; 
  26.             } 
  27.             return true
  28.         } 

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

  1. using BenchmarkDotNet.Attributes; 
  2. using BenchmarkDotNet.Jobs; 
  3. using BenchmarkDotNet.Running; 
  4. using System; 
  5. using System.Buffers.Binary; 
  6. using System.Runtime.InteropServices; 
  7. using System.Text; 
  8.  
  9. namespace BenTest 
  10.     [SimpleJob(RuntimeMoniker.NetCoreApp31)] 
  11.     [SimpleJob(RuntimeMoniker.CoreRt31)] 
  12.     [RPlotExporter] 
  13.     public class Test 
  14.     { 
  15.         private byte[] _a = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666"); 
  16.         private byte[] _b = Encoding.UTF8.GetBytes("5456456456444444444444156456454564444444444444444444444444444444444444444777777777777777777777711111111111116666666666666"); 
  17.  
  18.         private int[] A1 = new int[] { 4154444444878418787441574584897878978154854848787415489748788489787845648574897848548947 }; 
  19.         private int[] B2 = new int[] { 4154444444878418787441574584897878978154854848787415489748788489787845648574897848548947 }; 
  20.  
  21.         [Benchmark] 
  22.         public bool ForBytes() 
  23.         { 
  24.             for (int i = 0; i < _a.Length; i++) 
  25.             { 
  26.                 if (_a[i] != _b[i]) return false
  27.             } 
  28.             return true
  29.         } 
  30.  
  31.         [Benchmark] 
  32.         public bool ForArray() 
  33.         { 
  34.             return ForArray(A1, B2); 
  35.         } 
  36.  
  37.         private bool ForArray(T[] b1, T[] b2) where T : struct 
  38.         { 
  39.             for (int i = 0; i < b1.Length; i++) 
  40.             { 
  41.                 if (!b1[i].Equals(b2[i])) return false
  42.             } 
  43.             return true
  44.         } 
  45.  
  46.         [Benchmark] 
  47.         public bool EqualsArray() 
  48.         { 
  49.             return EqualArray(A1, B2); 
  50.         } 
  51.  
  52.         [Benchmark] 
  53.         public bool EqualsBytes() 
  54.         { 
  55.             var a = _a.AsSpan(); 
  56.             var b = _b.AsSpan(); 
  57.             Span<byte> copy1 = default
  58.             Span<byte> copy2 = default
  59.  
  60.             if (a.Length != b.Length) 
  61.                 return false
  62.  
  63.             for (int i = 0; i < a.Length;) 
  64.             { 
  65.                 if (a.Length - 8 > i) 
  66.                 { 
  67.                     copy1 = a.Slice(i, 8); 
  68.                     copy2 = b.Slice(i, 8); 
  69.                     if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) 
  70.                         return false
  71.                     i += 8
  72.                     continue
  73.                 } 
  74.  
  75.                 if (a[i] != b[i]) 
  76.                     return false
  77.                 i++; 
  78.             } 
  79.             return true
  80.         } 
  81.  
  82.         private bool EqualArray(T[] t1, T[] t2) where T : struct 
  83.         { 
  84.             Span<byte> b1 = MemoryMarshal.AsBytes(t1.AsSpan()); 
  85.             Span<byte> b2 = MemoryMarshal.AsBytes(t2.AsSpan()); 
  86.  
  87.             Span<byte> copy1 = default
  88.             Span<byte> copy2 = default
  89.  
  90.             if (b1.Length != b2.Length) 
  91.                 return false
  92.  
  93.             for (int i = 0; i < b1.Length;) 
  94.             { 
  95.                 if (b1.Length - 8 > i) 
  96.                 { 
  97.                     copy1 = b1.Slice(i, 8); 
  98.                     copy2 = b2.Slice(i, 8); 
  99.                     if (BinaryPrimitives.ReadUInt64BigEndian(copy1) != BinaryPrimitives.ReadUInt64BigEndian(copy2)) 
  100.                         return false
  101.                     i += 8
  102.                     continue
  103.                 } 
  104.  
  105.                 if (b1[i] != b2[i]) 
  106.                     return false
  107.                 i++; 
  108.             } 
  109.             return true
  110.         } 
  111.     } 
  112.  
  113.     class Program 
  114.     { 
  115.         static void Main(string[] args) 
  116.         { 
  117.             var summary = BenchmarkRunner.Run(); 
  118.             Console.ReadKey(); 
  119.         } 
  120.     } 

使用 BenchmarkDotNet 的测试结果如下:

  1. BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1052 (21H1/May2021Update) 
  2. Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores 
  3. .NET SDK=5.0.301 
  4.   [Host]        : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT 
  5.   .NET Core 3.1 : .NET Core 3.1.16 (CoreCLR 4.700.21.26205, CoreFX 4.700.21.26205), X64 RyuJIT 
  6.  
  7.  
  8. |      Method |           Job |       Runtime |     Mean |    Error |   StdDev | 
  9. |------------ |-------------- |-------------- |---------:|---------:|---------:| 
  10. |    ForBytes | .NET Core 3.1 | .NET Core 3.1 | 76.95 ns | 0.064 ns | 0.053 ns | 
  11. |    ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.37 ns | 1.258 ns | 1.177 ns | 
  12. | EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.91 ns | 0.027 ns | 0.024 ns | 
  13. | EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 26.26 ns | 0.432 ns | 0.383 ns | 

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

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

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

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

更高性能

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

  1. [Benchmark] 
  2.         public bool SpanEqual() 
  3.         { 
  4.             return SpanEqual(_a,_b); 
  5.         } 
  6.         private bool SpanEqual(byte[] a, byte[] b) 
  7.         { 
  8.             return a.AsSpan().SequenceEqual(b); 
  9.         } 

可以试试

  1. StructuralComparisons.StructuralEqualityComparer.Equals(a, b); 

性能测试结果:

  1. |      Method |           Job |       Runtime |      Mean |     Error |    StdDev | 
  2. |------------ |-------------- |-------------- |----------:|----------:|----------:| 
  3. |    ForBytes | .NET Core 3.1 | .NET Core 3.1 | 77.025 ns | 0.0502 ns | 0.0419 ns | 
  4. |    ForArray | .NET Core 3.1 | .NET Core 3.1 | 66.192 ns | 0.6127 ns | 0.5117 ns | 
  5. | EqualsArray | .NET Core 3.1 | .NET Core 3.1 | 17.897 ns | 0.0122 ns | 0.0108 ns | 
  6. | EqualsBytes | .NET Core 3.1 | .NET Core 3.1 | 25.722 ns | 0.4584 ns | 0.4287 ns | 
  7. |   SpanEqual | .NET Core 3.1 | .NET Core 3.1 |  4.736 ns | 0.0099 ns | 0.0093 ns | 

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

对于 C# 中的二进制处理技巧就介绍到这里,阅读 CLR 源码 时,我们可以学习到很多骚操作,读者可以多阅读 CLR 源码,对技术提升有很大的帮助。

来源:博客园内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯