高效地将字节数组转换为十进制数

6
如果我有一个字节数组,想要将该数组中一个连续的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)数组输入到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倍,我已经使用了一堆极端值进行测试,以确保它可以正常工作,但是十进制表示法不像其他原始类型那样直接,所以我还没有完全相信它可以处理所有可能的十进制值。
理论上,有一种方法可以将这16个连续字节复制到内存中的其他位置,并声明为Decimal,而无需任何检查。是否有人知道如何做到这一点?
(只有一个问题:尽管十进制数表示为16个字节,但某些可能的值并不构成有效的十进制数,因此进行未经检查的memcpy可能会破坏事情...)
还有其他更快的方法吗?

你有遇到过数组中连续出现多个小数的情况吗?如果没有,我想不到更快的方法了。 - Matthew Watson
2
问题并不在于BinaryReader非常慢,而是因为Decimal构造函数非常快。所以构造这些对象的开销在A / B测试中变得明显。安全和速度是相互冲突的目标。 - Hans Passant
1
@HansPassant 我并没有说 BinaryReader 很慢。但是,无论多快,通过不必要的间接方式显然都会减慢速度。如果一开始我就有了 BinaryReader 而不是一个字节数组,我怀疑调用它的 ReadDecimal 方法读取十进制数的速度不会更快。 - Evgeniy Berezovsky
2个回答

3

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

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

结果:时间:1.68秒序列化,1.81秒反序列化。这是我们的基准线。我还尝试了Buffer.BlockCopy到一个int[4],它为反序列化提供了0.42秒。使用问题中描述的方法,反序列化降至0.29秒。

然而,在理论上,可能有一种方法将这16个连续字节复制到内存中的其他位置,并声明其为Decimal,而不进行任何检查。是否有人知道如何做到这一点?

好吧,最快的方法是使用不安全的代码,这在这里没问题,因为十进制是值类型:

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

目前为止,我们得到的结果是:序列化用时 0.07 秒,反序列化用时 0.16 秒。我很确定这已经是最快的速度了……但是需要注意的是,这里存在安全隐患,而且我假设数据读写方式相同。

3

@Eugene Beresovksy 从流中读取数据是非常昂贵的。MemoryStream 虽然是一种强大而多用途的工具,但是直接读取二进制数组的成本相当高。也许正是因为这个原因,第二种方法表现更好。

我有一个第三种解决方案,但在我写出来之前,需要说明的是,我还没有测试过它的性能。

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

经过修改,这个解决方案可能不仅表现更好,而且也没有这个问题:"(只有一个问题:虽然小数被表示为16个字节,但其中一些可能的值并不构成有效的小数,因此进行未经检查的memcpy可能会破坏事情...)"


尽管你的解决方案更为直接,但奇怪的是它只有我的解决方案的一半速度。请阅读我问题中两段代码片段之间的文本,你会发现我已经尝试过这种方法,虽然没有在代码中明确表述,因为它的性能不佳。就正确性而言,除非有错误(例如需要添加范围检查),否则它与我的解决方案一样好或一样差,而这个错误应该是可以修复的,并且会带来一个性能成本,我不认为会让它比使用 new Decimal(Int32[]) 解决方案慢。 - Evgeniy Berezovsky
@EugeneBeresovsky 我知道这是一篇旧帖子,但我想知道你是否尝试过变量 int[] tmp = new int[4]; Buffer.BlockCopy(src, offset, tmp, 0, 16); return new decimal(tmp);BitConverter 的速度相当慢,所以这个解决方案可能更好一些。 - atlaste

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接