【问题标题】:Improving performance converting bytes into UInt32提高将字节转换为 UInt32 的性能
【发布时间】:2021-04-30 21:13:29
【问题描述】:

我正在研究处理 2GB 数据的源代码,它代表 60 秒的网络流量。总处理时间约为 40 秒。我正在尝试尽可能优化我的代码以获得最佳性能,以尝试将总处理时间控制在 30 秒以下。

我目前在 dotTrace 中的分析表明,在我的代码进行的 330 万次调用中,有 7.62% 的时间花在了 Timestamp 结构的构造函数中。

具体来说,有两个陈述我正在努力改进:

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

这是完整的结构:

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();
    }
}

ReverseByteOrder 方法似乎不会产生太大的性能损失,因为根据 dotTrace,它所代表的时间不到 0.5%,但此处仅供参考:

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);
    }
}

【问题讨论】:

  • 为什么不创建一个ReverseByteToUInt32 扩展方法呢?将字节转换为 UInt32 只是将它们转换回字节,将它们反转,然后再次将它们转换为 UInt32 似乎很浪费。
  • 我同意,但是,这种情况很少见,reverseByteOrder 标志几乎总是设置为 false。
  • 在 dotnet core 中,我会使用一种二进制原语读/写方法来避免创建数组和转换字节顺序。 docs.microsoft.com/en-us/dotnet/api/…

标签: c# .net performance .net-4.0 c#-7.3


【解决方案1】:

看起来您正在做很多工作来对抗字节顺序。老实说,这就是BitConverter 落在它脸上的地方。好消息是,在现代运行时我们有 BinaryPrimitives,它具有 endian 感知 操作,然后是 JIT 优化的。含义:编写检查 CPU 字节序,但在 JIT 期间该检查被删除,只保留与 CPU 相关的代码。所以:避免BitConverter。这确实需要对您的代码进行一些修改,因为reverseByteOrder 不再是输入,但请考虑:

(注意:您可以将byte[] 作为Span&lt;byte&gt;/ReadOnlySpan&lt;byte&gt; 传递——它是隐式的)

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));
    // ...
}

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

【讨论】:

  • 该项目目前是在 4.7.2 基础上编写的,但我认为除了继续使用 5.0 以利用所有新类型和方法之外别无选择。谢谢!
  • @jscarle 它们都在 NuGet 包中可用 - System.Memory、System.Buffers 等;但是,.NET Framework 并没有获得 .NET Core 所做的所有优化 - JIT 不认为它是特殊的,等等
  • 可以在一些有用的情况下帮助内联 ...是否有一些关于此的博客/一些文档?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-10-18
  • 1970-01-01
  • 2021-08-12
  • 1970-01-01
  • 2017-07-13
  • 2014-03-23
  • 1970-01-01
相关资源
最近更新 更多