Search code examples
c#.netperformance.net-4.0c#-7.3

Improving performance converting bytes into UInt32


I'm working on source code which processes 2GB of data which represents 60 seconds of network traffic. The total processing time is around 40 seconds. I'm trying to optimize my code for performance as best as possible to try to bring total processing time under the 30 second mark.

Thread Time

My current analysis in dotTrace shows that 7.62% of the time of the 3.3 million calls my code makes is being spent within the constructor of my Timestamp struct.

Constructor

Specifically, there are two statements which I'm trying to improve:

TimestampHigh = BitConverter.ToUInt32(timestampBytes, 0);
TimestampLow = BitConverter.ToUInt32(timestampBytes, 4);

Here is the full struct:

public readonly struct Timestamp
{
    public uint TimestampHigh { get; }
    public uint TimestampLow { get; }
    public uint Seconds { get; }
    public uint Microseconds { get; }
    public DateTime LocalTime => new DateTime(EpochTicks + _ticks, DateTimeKind.Utc).ToLocalTime();

    private const ulong MicrosecondsPerSecond = 1000000UL;
    private const ulong HighFactor = 4294967296UL;
    private readonly ulong _timestamp;

    private const long EpochTicks = 621355968000000000L;
    private const long TicksPerMicrosecond = 10L;
    private readonly long _ticks;

    public Timestamp(byte[] timestampBytes, bool reverseByteOrder)
    {
        if (timestampBytes == null)
            throw new ArgumentNullException($"{nameof(timestampBytes)} cannot be null.");
        if (timestampBytes.Length != 8)
            throw new ArgumentException($"{nameof(timestampBytes)} must have a length of 8.");

        TimestampHigh = BitConverter.ToUInt32(timestampBytes, 0).ReverseByteOrder(reverseByteOrder);
        TimestampLow = BitConverter.ToUInt32(timestampBytes, 4).ReverseByteOrder(reverseByteOrder);
        _timestamp = ((ulong)TimestampHigh * HighFactor) + (ulong)TimestampLow;
        _ticks = (long)_timestamp * TicksPerMicrosecond;
        Seconds = (uint)(_timestamp / MicrosecondsPerSecond);
        Microseconds = (uint)(_timestamp % MicrosecondsPerSecond);
    }

    public Timestamp(uint seconds, uint microseconds)
    {
        Seconds = seconds;
        Microseconds = microseconds;
        _timestamp = seconds * MicrosecondsPerSecond + microseconds;
        _ticks = (long)_timestamp * TicksPerMicrosecond;
        TimestampHigh = (uint)(_timestamp / HighFactor);
        TimestampLow = (uint)(_timestamp % HighFactor);
    }

    public byte[] ConvertToBytes(bool reverseByteOrder)
    {
        List<byte> bytes = new List<byte>();
        bytes.AddRange(BitConverter.GetBytes(TimestampHigh.ReverseByteOrder(reverseByteOrder)));
        bytes.AddRange(BitConverter.GetBytes(TimestampLow.ReverseByteOrder(reverseByteOrder)));

        return bytes.ToArray();
    }

    public bool Equals(Timestamp other)
    {
        return TimestampLow == other.TimestampLow && TimestampHigh == other.TimestampHigh;
    }

    public static bool operator ==(Timestamp left, Timestamp right)
    {
        return left.Equals(right);
    }

    public static bool operator !=(Timestamp left, Timestamp right)
    {
        return !left.Equals(right);
    }

    public override bool Equals(object obj)
    {
        return obj is Timestamp other && Equals(other);
    }

    public override int GetHashCode()
    {
        return _timestamp.GetHashCode();
    }
}

The method ReverseByteOrder does not seem to incur much of a performance penalty as it represents less than 0.5% of the time according to dotTrace, however here it is for reference:

public static UInt32 ReverseByteOrder(this UInt32 value, bool reverseByteOrder)
{
    if (!reverseByteOrder)
    {
        return value;
    }
    else
    {
        byte[] bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        return BitConverter.ToUInt32(bytes, 0);
    }
}

Solution

  • It looks like you're doing a lot of work to fight endianness. That is where BitConverter falls on it's face, honestly. The good news is that in modern runtimes we have BinaryPrimitives, which has endian aware operations that are then JIT-optimized. Meaning: it is written with a check on CPU-endianness, but that check gets removed during JIT, with just the CPU-relevant code being retained. So: avoid BitConverter. This does require a little rework in your code, as the reverseByteOrder is no longer an input, but consider:

    (note: you can pass a byte[] in as a Span<byte>/ReadOnlySpan<byte> - it is implicit)

    public Timestamp(ReadOnlySpan<byte> timestampBytes)
    {
        static void ThrowInvalidLength() // can help inlining in some useful cases
                => throw new ArgumentException($"{nameof(timestampBytes)} must have a length of 8.");
        if (timestampBytes.Length != 8) ThrowInvalidLength();
    
         TimestampHigh = BinaryPrimitives.ReadUInt32BigEndian(timestampBytes);
         TimestampLow = BinaryPrimitives.ReadUInt32BigEndian(timestampBytes.Slice(4));
        // ...
    }
    

    and

    public void ConvertToBytes(Span<byte> destination)
    {
        BinaryPrimitives.WriteUInt32BigEndian(destination, TimestampHigh);
        BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(4), TimestampLow);
    }