HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpModerate

Performance Byte[] to Generic

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
byteperformancegeneric

Problem

I'm in need to make a byte[] -> T extension method and need it to be fast (no need for it being pretty)

This function will be called 100's of 1000's of times in very short succession in an absolute performance critical environment.

We're currently optimizing on "ticks" level, every tick translates to a couple milliseconds higher in the callstack, thus the need of raw speed over maintainability (not how I like to design software, but the reasoning behind this is out of scope).

Consider the following code, it's clean and maintainable, but it's relatively slow (probably due to boxing and unboxing), Can this be optimized to be faster?

public static T ConvertTo(this byte[] bytes, int offset = 0)
{
    var type = typeof(T);
    if (type == typeof(sbyte)) return bytes[offset].As();
    if (type == typeof(byte)) return bytes[offset].As();
    if (type == typeof(short)) return BitConverter.ToInt16(bytes, offset).As();
    if (type == typeof(ushort)) return BitConverter.ToUInt32(bytes, offset).As();
    if (type == typeof(int)) return BitConverter.ToInt32(bytes, offset).As();
    if (type == typeof(uint)) return BitConverter.ToUInt32(bytes, offset).As();
    if (type == typeof(long)) return BitConverter.ToInt64(bytes, offset).As();
    if (type == typeof(ulong)) return BitConverter.ToUInt64(bytes, offset).As();

    throw new NotImplementedException();
}

public static T As(this object o)
{
    return (T)o;
}


And yes it needs to be generic, sadly

Solution

First, you seem to have a minor typo on this line:

if (type == typeof(ushort)) return BitConverter.ToUInt32(bytes, offset).As();


That should be:

if (type == typeof(ushort)) return BitConverter.ToUInt16(bytes, offset).As();


Just as well, you have a bug in here that is a pretty big one (causes exception on any attempts to convert anything to sbyte with your method):

if (type == typeof(sbyte)) return bytes[offset].As();


Should be:

if (type == typeof(sbyte)) return ((sbyte)bytes[offset]).As();


If you really need speed, you should probably not use the BitConverter class in this situation. Use bitwise operators, as they are much faster.

See this answer for a comparison.

You should also not use .As either, and instead cast within the method using: (T)(object). This eliminates unnecessary stack overhead.

Two improvements we can make immediately:

  • Replace all the BitConverter work with bitwise work.



  • Replace all the .As() with (T)(object) casts instead.



Performance Evaluation

The performance difference, when those changes are made is significant for certain types of T.

ConvertTo1 on 50000000 rounds, time (in ms) taken: 3465
ConvertTo1 on 50000000 rounds, time (in ms) taken: 4217
ConvertTo1 on 50000000 rounds, time (in ms) taken: 5586
ConvertTo1 on 50000000 rounds, time (in ms) taken: 7665
ConvertTo2 on 50000000 rounds, time (in ms) taken: 4995
ConvertTo2 on 50000000 rounds, time (in ms) taken: 5775
ConvertTo2 on 50000000 rounds, time (in ms) taken: 6945
ConvertTo2 on 50000000 rounds, time (in ms) taken: 8492
ConvertToInt on 50000000 rounds, time (in ms) taken: 1092

Verifying results of conversions are same:
SByte test: 1, 1: True
Byte test: 1, 1: True
Short test: 10497, 10497: True
UShort test: 10497, 10497: True
Int test: 755378433, 755378433: True
UInt test: 755378433, 755378433: True
Long test: 4041804345027995905, 4041804345027995905: True
ULong test: 4041804345027995905, 4041804345027995905: True


Now, to explain, ConvertTo1 is the optimized method, ConvertTo2 is the original, and ConvertToInt is a strongly-typed method for converting directly to int.

By removing .As() and replacing with (T)(object), and replacing all the BitConverter work with bitwise work, we cut our work down to:

  • 69% original time for byte



  • 73% original time for short



  • 80% original time for int



  • 90% original time for long



Also note that we returned the exact same values for all situations.

Just how bad is performance dragging because of the boxing?

Badly. If we consider another method, ConvertTo3 which is strongly typed around an int parameter, we can come up with the following result:

ConvertToInt on 50000000 rounds, time (in ms) taken: 1034


Now that is with a method that only returns an int value.

This leads us to be able to conclude that, due to the boxing of the generic types, and recasting them, we lose a lot of performance. (Around 500% or so right-off-the-bat.) The issues isn't the fact that you use generics, the issue is that you use boxing several times within, which creates extra overhead.

Using an .As() method

While it would make sense to think that this would be fairly quick, with no drawbacks, this is actually much slower than inline calls due to the overhead of having an additional method call. Replacing .As() with (T)(object) right out of the gate gave us significant performance back. It may even be acceptable to only make that change and continue using BitConverter.

In fact, upon further research, it seems that if you replace the ConvertTo2 method I supplied with this variant:

public static T ConvertTo2(this byte[] bytes, int offset = 0)
    {
        var type = typeof(T);
        if (type == typeof(sbyte)) return (T)(object)((sbyte)bytes[offset]);
        if (type == typeof(byte)) return (T)(object)bytes[offset];
        if (type == typeof(short)) return (T)(object)BitConverter.ToInt16(bytes, offset);
        if (type == typeof(ushort)) return (T)(object)BitConverter.ToUInt16(bytes, offset);
        if (type == typeof(int)) return (T)(object)BitConverter.ToInt32(bytes, offset);
        if (type == typeof(uint)) return (T)(object)BitConverter.ToUInt32(bytes, offset);
        if (type == typeof(long)) return (T)(object)BitConverter.ToInt64(bytes, offset);
        if (type == typeof(ulong)) return (T)(object)BitConverter.ToUInt64(bytes, offset);

        throw new NotImplementedException();
    }


The performance difference between ConvertTo1 and ConvertTo2 is very minimal. (Though, removing the BitConverter work still does seem to make it slightly faster, the difference is very insignificant at this level.)

Food for thought.

Code

Here's the code I used for conversions:

```
public static class Extensions
{
public static T ConvertTo1(this byte[] bytes, int offset = 0)
{
var type = typeof(T);
if (type == typeof(sbyte)) return (T)(object)((sbyte)bytes

Code Snippets

if (type == typeof(ushort)) return BitConverter.ToUInt32(bytes, offset).As<T>();
if (type == typeof(ushort)) return BitConverter.ToUInt16(bytes, offset).As<T>();
if (type == typeof(sbyte)) return bytes[offset].As<T>();
if (type == typeof(sbyte)) return ((sbyte)bytes[offset]).As<T>();
ConvertTo1<byte> on 50000000 rounds, time (in ms) taken: 3465
ConvertTo1<short> on 50000000 rounds, time (in ms) taken: 4217
ConvertTo1<int> on 50000000 rounds, time (in ms) taken: 5586
ConvertTo1<long> on 50000000 rounds, time (in ms) taken: 7665
ConvertTo2<byte> on 50000000 rounds, time (in ms) taken: 4995
ConvertTo2<short> on 50000000 rounds, time (in ms) taken: 5775
ConvertTo2<int> on 50000000 rounds, time (in ms) taken: 6945
ConvertTo2<long> on 50000000 rounds, time (in ms) taken: 8492
ConvertToInt on 50000000 rounds, time (in ms) taken: 1092

Verifying results of conversions are same:
SByte test: 1, 1: True
Byte test: 1, 1: True
Short test: 10497, 10497: True
UShort test: 10497, 10497: True
Int test: 755378433, 755378433: True
UInt test: 755378433, 755378433: True
Long test: 4041804345027995905, 4041804345027995905: True
ULong test: 4041804345027995905, 4041804345027995905: True

Context

StackExchange Code Review Q#101636, answer score: 15

Revisions (0)

No revisions yet.