使用BitConverter在C#中进行快速转换,速度是否可以更快?

16

在我们的应用程序中,有一个非常大的字节数组,我们需要将这些字节转换为不同的类型。目前,我们使用BitConverter.ToXXXX()来实现这个目的。我们主要涉及的是ToInt16ToUInt64

对于UInt64,我们的问题在于数据流实际上有6个字节的数据来表示一个大整数。由于没有本地函数可以将6个字节的数据转换为UInt64,所以我们做如下操作:

UInt64 value = BitConverter.ToUInt64() & 0x0000ffffffffffff;

我们使用的ToInt16更简单,因此不需要进行任何位操作。

由于我们执行这两个操作太多了,我想问问SO社区是否有更快的方法进行转换。现在,约20%的CPU周期被这两个函数消耗。


8
整数计算性能不太可能是您的问题。处理大型数组几乎总是会使缓慢的RAM总线成为瓶颈。请关注您分析器输出中的“最后一级缓存未命中”性能计数器。 - Hans Passant
@Hans:你肯定是对的:我们受到内存限制。但是为此,我不知道该怎么办。我们有一个大型数组,必须遍历每个字节以提取数据。当我在数组中按线性方式进行时,硬件预取器可能会锁定访问模式,除此之外,我不知道还能做什么。--谢谢 - SomethingBetter
5个回答

8

您是否考虑过直接使用内存指针?我不能保证其性能,但这是C++\C中常用的技巧...

        byte[] arr = { 1, 2, 3, 4, 5, 6, 7, 8 ,9,10,11,12,13,14,15,16};

        fixed (byte* a2rr = &arr[0])
        {

            UInt64* uint64ptr = (UInt64*) a2rr;
            Console.WriteLine("The value is {0:X2}", (*uint64ptr & 0x0000FFFFFFFFFFFF));
            uint64ptr = (UInt64*) ((byte*) uint64ptr+6);
            Console.WriteLine("The value is {0:X2}", (*uint64ptr & 0x0000FFFFFFFFFFFF));
        }

您需要在“构建设置”中将您的程序集设置为“不安全”,并标记您将使用此方法不安全的方法。这种方法也与小端相关。


这证明目前为止这是最快的方法。 - SomethingBetter
3
要小心处理这个问题。如果你想读取数组末尾的一个六字节数字,你可能会遇到异常。举例来说,如果上述的数组长度只有12个字节,当你尝试读取第二个数字时,就会产生异常。 - Jim Mischel

5
你可以使用 System.Buffer 类来进行整个数组到另一个不同类型的数组的快速'块拷贝'操作:

BlockCopy方法通过内存偏移而不是编程构造(如索引或上下限数组边界)访问src参数数组中的字节。

数组类型必须是“基元”类型,它们必须对齐,并且复制操作是大小端敏感的。在你的6字节整数情况下,除非你可以获取每六个字节的源数组并添加两个字节的填充,使其对齐到Int64,否则它无法与.NET的任何“基元”类型对齐。但这个方法可以用于Int16数组,这可能会加速一些操作。

感谢提供 System.Buffer.BlockCopy 的信息。在我们的情况下,UInt64 和 Int16 交错存储在数组中,因此 BlockCopy 对我们无效,但这些信息对我们很有帮助,我们可以在将来使用这种方法。 - SomethingBetter

2

为什么不:

UInt16 valLow = BitConverter.ToUInt16();
UInt64 valHigh = (UInt64)BitConverter.ToUInt32();
UInt64 Value = (valHigh << 16) | valLow;

你可以将其转换为单个语句,尽管JIT编译器可能会自动为您完成此操作。
这将防止您读取那些最终要丢弃的额外两个字节。
如果这不能减少CPU使用率,那么您可能需要编写自己的转换器,直接从缓冲区中读取字节。您可以使用数组索引或者如果认为有必要,使用指针的不安全代码。
请注意,正如评论者所指出的,如果您使用任何这些建议,则要么您受限于特定的“端序”,要么您将不得不编写代码来检测大小端并相应地做出反应。我上面展示的代码示例适用于小端(x86)。

2
你应该提到这适用于给定的字节序(我认为是小端,但我总是混淆两者)。这可能对问题发起人有所影响,也可能没有。 - R. Martinho Fernandes
我按照你最初的建议去做,认为读取那2个额外的字节并将其丢弃会减慢我的速度,但结果证明,这是最慢的方法。我想既然数据已经被缓存,每次读取8个或6个字节并没有什么区别。你的第二个建议,即@Jimmy提供的代码作为答案,运行得更快。--谢谢 - SomethingBetter

2

您可以在这里查看我对类似问题的回答。

这是与Jimmy的答案相同的不安全内存操作,但以更加“友好”的方式呈现给用户。它将允许您将byte数组视为UInt64数组。


0

对于其他遇到这个问题的人,如果你只需要小端序并且不需要自动检测大端序并从中转换,那么我已经编写了一个扩展版本的BitConverter,其中包含许多添加项来处理Span以及转换类型为T的数组,例如int[]或timestamp[]。

还扩展了支持的类型,包括时间戳、十进制和日期时间。

https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Serialization/BitConverterExtended.cs

使用示例:

Random rnd = new Random();
RentedBuffer<byte> buffer = RentedBuffer<byte>.Shared.Rent(BitConverterExtended.SizeOfUInt64
    + (20 * BitConverterExtended.SizeOfUInt16)
    + (20 * BitConverterExtended.SizeOfTimeSpan)
    + (10 * BitConverterExtended.SizeOfSingle);
UInt64 exampleLong = long.MaxValue;
int startIndex = 0;
startIndex += BitConverterExtended.GetBytes(exampleLong, buffer.BufferSpan, startIndex);

UInt16[] shortArray = new UInt16[20];
for (int I = 0; I < shortArray.Length; I++) { shortArray[I] = (ushort)rnd.Next(0, UInt16.MaxValue); }
//When using reflection / expression trees CLR cannot distinguish between UInt16 and Int16 or Uint64 and Int64 etc...
//Therefore Uint methods are renamed.
startIndex += BitConverterExtended.GetBytesUShortArray(shortArray, buffer.BufferSpan, startIndex);

TimeSpan[] timespanArray = new TimeSpan[20];
for (int I = 0; I < timespanArray.Length; I++) { timespanArray[I] = TimeSpan.FromSeconds(rnd.Next(0, int.MaxValue)); }
startIndex += BitConverterExtended.GetBytes(timespanArray, buffer.BufferSpan, startIndex);

float[] floatArray = new float[10];
for (int I = 0; I < floatArray.Length; I++) { floatArray[I] = MathF.PI * rnd.Next(short.MinValue, short.MaxValue); }
startIndex += BitConverterExtended.GetBytes(floatArray, buffer.BufferSpan, startIndex);

//Do stuff with buffer and then
buffer.Return(); //always better to return it as soon as possible
//Or in case you forget
buffer = null;
//and let RentedBufferContract do this automatically

它支持从byte[]或RentedBuffer读取和写入,但使用RentedBuffer类可以大大减少GC收集开销。 RentedBufferContract类在内部处理返回缓冲区到池中以防止内存泄漏。

还包括一个类似于MessagePack的序列化器。 注意:MessagePack是一种更快速、更具特色的序列化器,但此序列化器通过从租用的字节缓冲区读取和写入来减少GC收集开销。

https://github.com/tcwicks/ChillX/blob/master/src/ChillX.Serialization/ChillXSerializer.cs


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接