Fast casting in C# using BitConverter, can it be any faster?
Asked Answered
C

5

16

In our application, we have a very large byte-array and we have to convert these bytes into different types. Currently, we use BitConverter.ToXXXX() for this purpose. Our heavy hitters are, ToInt16 and ToUInt64.

For UInt64, our problem is that the data stream has actually 6-bytes of data to represent a large integer. Since there is no native function to convert 6-bytes of data to UInt64, we do:

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

Our use of ToInt16 is simpler, do don't have to do any bit manipulation.

We do so many of these 2 operations that I wanted to ask the SO community whether there's a faster way to do these conversions. Right now, approximately 20% of our entire CPU cycles is consumed by these two functions.

Concurrence answered 7/2, 2011 at 16:41 Comment(2)
Integer performance isn't likely to be your problem. Processing large arrays almost always makes the slow RAM bus the bottleneck. Pay attention to the "Last Level Cache Misses" performance counter in your profiler output.Covariance
@Hans: You're definitely correct: We are memory bound. But for that, I don't know what do to. We have a large array and we must traverse each byte to extract the data. As I go linearly in the array, the hardware prefetcher probably locks in to the access pattern and beyond that, I don't know what else can be done. --thanksConcurrence
V
8

Have you thought about using memory pointers directly. I can't vouch for its performance but it is a common trick in 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));
        }

You'll need to make your assembly "unsafe" in the build settings as well as mark the method in which you'd be doing this unsafe aswell. You are also tied to little endian with this approach.

Vookles answered 7/2, 2011 at 19:15 Comment(2)
This turned out to be the fastest way of doing it, at least so far.Concurrence
Be careful with this. If you want to read one of those 6-byte numbers at the end of an array, you're going to get an exception. That is, if in the example above the array was only 12 bytes long, you'd get an exception when reading the second value.Streaky
D
5

You can use the System.Buffer class to copy a whole array over to another array of a different type as a fast, 'block copy' operation:

The BlockCopy method accesses the bytes in the src parameter array using offsets into memory, not programming constructs such as indexes or upper and lower array bounds.

The array types must be of 'primitive' types, they must align, and the copy operation is endian-sensitive. In your case of 6-bytes integers, it can't align with any of .NET's 'primitive' types, unless you can obtain the source array with two bytes of padding for each six, which will then align to Int64. But this method will work for arrays of Int16, which may speed up some of your operations.

Dm answered 7/2, 2011 at 20:21 Comment(1)
Thanks for the System.Buffer.BlockCopy info. In our case, the UInt64 and Int16s are interleaved in the array, so BlockCopy won't work for us, but this information was helpful, we can use this method in the future.Concurrence
S
2

Why not:

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

You can make that a single statement, although the JIT compiler will probably do that for you automatically.

That will prevent you from reading those extra two bytes that you end up throwing away.

If that doesn't reduce CPU, then you'll probably want to write your own converter that reads the bytes directly from the buffer. You can either use array indexing or, if you think it's necessary, unsafe code with pointers.

Note that, as a commenter pointed out, if you use any of these suggestions, then either you're limited to a particular "endian-ness", or you'll have to write your code to detect little/big endian and react accordingly. The code sample I showed above works for little endian (x86).

Streaky answered 7/2, 2011 at 16:55 Comment(2)
You should mention that this works for a given endianness (I think little, but I'm always mixing up the two). It may or may not matter to the OP.Peepul
I followed your initial suggestion thinking that reading those 2 extra bytes and throwing them out must be slowing me down, but turned out, this is the slowest way of doing it. I guess since the data is already cached it doesn't really matter to read 8 or 6 bytes at a time. Your second suggestion, the code @Vookles provided as an answer works much faster. -- thanksConcurrence
W
2

See my answer for a similar question here. It's the same unsafe memory manipulation as in Jimmy's answer, but in a more "friendly" way for consumers. It'll allow you to view your byte array as UInt64 array.

Whatnot answered 10/5, 2012 at 3:59 Comment(0)
N
0

For anyone else who stumbles across this if you only need little endian and do not need to auto detect big endian and convert from that. Then I've written an extended version of bitconverter with a number of additions to handle Span as well as converting arrays of type T for example int[] or timestamp[]

Also extended the types supported to include timestamp, decimal and datetime.

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

Example usage:

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

it supports reading from and writing to both byte[] or RentedBuffer however using the RentedBuffer class greatly reduces GC collection overheads. RentedBufferContract class internally handles returning buffers to the pool to prevent memory leaks.

Also includes a serializer which is similar to messagepack. Note: MessagePack is a faster serializer with more features however this serializer reduces GC collection overheads by reading from and writing to rented byte buffers.

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

Necrophilia answered 3/5, 2022 at 14:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.