【问题标题】:Efficiently convert byte array to Decimal有效地将字节数组转换为十进制
【发布时间】:2013-06-07 08:07:10
【问题描述】:

如果我有一个字节数组,并且想要将该数组的连续 16 字节块(包含 .net 的 Decimal 表示)转换为正确的 Decimal 结构,那么最有效的方法是什么?

在我正在优化的情况下,这是在我的分析器中显示为最大 CPU 消耗者的代码。

public static decimal ByteArrayToDecimal(byte[] src, int offset)
{
    using (MemoryStream stream = new MemoryStream(src))
    {
        stream.Position = offset;
        using (BinaryReader reader = new BinaryReader(stream))
            return reader.ReadDecimal();
    }
}

为了摆脱MemoryStreamBinaryReader,我认为将BitConverter.ToInt32(src, offset + x)s 的数组输入Decimal(Int32[]) 构造函数会比我在下面提供的解决方案更快,但下面的版本很奇怪,快两倍。

const byte DecimalSignBit = 128;
public static decimal ByteArrayToDecimal(byte[] src, int offset)
{
    return new decimal(
        BitConverter.ToInt32(src, offset),
        BitConverter.ToInt32(src, offset + 4),
        BitConverter.ToInt32(src, offset + 8),
        src[offset + 15] == DecimalSignBit,
        src[offset + 14]);
}

这是MemoryStream/BinaryReader 组合的10 倍,我用一堆极值对其进行了测试以确保它有效,但十进制表示并不像其他原始类型,所以我还不相信它适用于 100% 的可能十进制值。

然而,理论上,有一种方法可以将这 16 个连续字节复制到内存中的其他位置,并声明为十进制,无需任何检查。有人知道这样做的方法吗?

(只有一个问题:虽然小数表示为 16 个字节,但一些可能的值不构成有效的小数,所以不检查memcpy 可能会破坏事情......)

或者还有其他更快的方法吗?

【问题讨论】:

  • 是否存在数组中连续有多个十进制值的情况?如果没有,我想不出更快的方法。
  • 这里的问题不是 BinaryReader 这么慢,而是 Decimal 构造函数非常快。因此,构建这些对象的开销在 A/B 测试中变得很明显。安全和速度是相互矛盾的目标。
  • @HansPassant 我没说BinaryReader 很慢。但是,不管它们有多快,经历任何不必要的间接显然都会减慢速度。如果我有一个 BinaryReader 开头,而不是一个字节数组,我怀疑有什么比调用它的 ReadDecimal 方法更快的方法从它读取小数。

标签: c# .net decimal


【解决方案1】:

尽管这是一个老问题,但我有点好奇,所以决定进行一些实验。让我们从实验代码开始。

static void Main(string[] args)
{
    byte[] serialized = new byte[16 * 10000000];

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000000; ++i)
    {
        decimal d = i;

        // Serialize
        using (var ms = new MemoryStream(serialized))
        {
            ms.Position = (i * 16);
            using (var bw = new BinaryWriter(ms))
            {
                bw.Write(d);
            }
        }
    }
    var ser = sw.Elapsed.TotalSeconds;

    sw = Stopwatch.StartNew();
    decimal total = 0;
    for (int i = 0; i < 10000000; ++i)
    {
        // Deserialize
        using (var ms = new MemoryStream(serialized))
        {
            ms.Position = (i * 16);
            using (var br = new BinaryReader(ms))
            {
                total += br.ReadDecimal();
            }
        }
    }
    var dser = sw.Elapsed.TotalSeconds;

    Console.WriteLine("Time: {0:0.00}s serialization, {1:0.00}s deserialization", ser, dser);
    Console.ReadLine();
}

结果:Time: 1.68s serialization, 1.81s deserialization。这是我们的基线。我还尝试将Buffer.BlockCopy 转换为int[4],这为我们提供了 0.42 秒的反序列化时间。使用问题中描述的方法,反序列化下降到 0.29s。

然而,理论上,可能有一种方法可以复制这 16 个连续的 字节到内存中的其他位置,并声明为十进制, 没有任何检查。有人知道这样做的方法吗?

嗯,是的,最快的方法是使用不安全的代码,这里没问题,因为小数是值类型:

static unsafe void Main(string[] args)
{
    byte[] serialized = new byte[16 * 10000000];

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 10000000; ++i)
    {
        decimal d = i;

        fixed (byte* sp = serialized)
        {
            *(decimal*)(sp + i * 16) = d;
        }
    }
    var ser = sw.Elapsed.TotalSeconds;

    sw = Stopwatch.StartNew();
    decimal total = 0;
    for (int i = 0; i < 10000000; ++i)
    {
        // Deserialize
        decimal d;
        fixed (byte* sp = serialized)
        {
            d = *(decimal*)(sp + i * 16);
        }

        total += d;
    }
    var dser = sw.Elapsed.TotalSeconds;

    Console.WriteLine("Time: {0:0.00}s serialization, {1:0.00}s deserialization", ser, dser);

    Console.ReadLine();
}

此时,我们的结果是:Time: 0.07s serialization, 0.16s deserialization。很确定这是最快的速度......不过,你必须在这里接受 unsafe ,而且我认为内容的编写方式与读取方式相同。

【讨论】:

    【解决方案2】:

    @Eugene Beresovksy 从流中读取非常昂贵。 MemoryStream 无疑是一个功能强大且用途广泛的工具,但直接读取二进制数组的成本相当高。也许正因为如此,第二种方法的效果更好。

    我有第 3 个解决方案给你,但在我写之前,有必要说我还没有测试过它的性能。

    public static decimal ByteArrayToDecimal(byte[] src, int offset)
    {
        var i1 = BitConverter.ToInt32(src, offset);
        var i2 = BitConverter.ToInt32(src, offset + 4);
        var i3 = BitConverter.ToInt32(src, offset + 8);
        var i4 = BitConverter.ToInt32(src, offset + 12);
    
        return new decimal(new int[] { i1, i2, i3, i4 });
    }
    

    这是一种基于二进制构建构建而无需担心System.Decimal 的规范的方法。它与默认的 .net 位提取方法相反:

    System.Int32[] bits = Decimal.GetBits((decimal)10);
    

    编辑:

    这个解决方案可能没有表现得更好,但也没有这个问题:"(There's only one problem: Although decimals are represented as 16 bytes, some of the possible values do not constitute valid decimals, so doing an uncheckedmemcpy could potentially break things...)"

    【讨论】:

    • 虽然你的解决方案更直接,但它的速度只有我的一半,这很奇怪。阅读我的问题中 2 个代码 sn-ps 之间的文本,您会发现我已经尝试过了,没有在代码中拼写出来,因为它没有执行。就正确性而言,它并不比我的解决方案更好或更差,除非其中存在错误(例如,需要添加范围检查),这应该是可修复的,并且会产生我怀疑会产生的性能成本与new Decimal(Int32[]) 解决方案一样“慢”。
    • @EugeneBeresovsky 我知道这是一篇旧帖子,但我想知道您是否尝试过int[] tmp = new int[4]; Buffer.BlockCopy(src, offset, tmp, 0, 16); return new decimal(tmp); 的变体。 BitConverter 很慢,所以这可能会提示这个解决方案的答案。
    猜你喜欢
    • 2013-05-22
    • 2021-11-07
    • 1970-01-01
    • 2013-02-18
    • 2016-01-13
    • 1970-01-01
    • 2012-09-14
    • 2018-03-02
    • 1970-01-01
    相关资源
    最近更新 更多