【问题标题】:Copy decimal into byte array without allocations将十进制复制到字节数组中而不进行分配
【发布时间】:2020-11-19 06:23:54
【问题描述】:

梦想

我正在寻找一种方法将十进制值复制到字节数组缓冲区中,然后无需任何堆分配即可将这些字节读回十进制。理想情况下,这不需要不安全的上下文。

我的尝试

我以前在 C# 中使用过临时联合来做一些疯狂的事情。这是一种非常酷的方式,可以随意随意读取内存,但您必须小心。您可能会进入变量我明确存在的损坏状态,例如byte[],但在调试器中查看的值是int[]。我什至不知道这样的事情是可能的!

注意:Marc 在下面的 cmets 中提出了一个非常重要的观点。由于字节顺序,您无法使用像这样的重叠结构概念可靠地将数字直接转换为字节。在这种情况下,您可以安全地使用整数,因为十进制类型在内部使用了 4 个整数。这是来自 protobuf-net 的十进制序列化程序的 [示例]。

1: struct union w/decimal and byte[]

第一次尝试尝试使用带有 decimalbyte[] 字段的结构联合概念,两者都在 0 偏移处,因此它们占用完全相同的内存位置。然后我可以写入一个字段并从另一个字段读取。

[StructLayout(LayoutKind.Explicit)]
private readonly struct convert_decimal_to_bytes
{
    [FieldOffset(0)]
    public readonly decimal value;
    [FieldOffset(0)]
    public readonly byte[] bytes;

    public convert_decimal_to_bytes(decimal value)
    {
        bytes = default;
        this.value = value;
    }
}

这甚至不会在没有抛出的情况下运行 - 类型无法加载以下内容:

System.TypeLoadException : Could not load type 'convert_decimal_to_bytes' from assembly 'teloneum, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.

显然 CLR 不喜欢它们被重叠——但这只是因为一个是值类型而另一个是引用类型。

2:结构联合,带十进制和 16 个字节!

所以我决定删除引用类型!现在,下面的工作,或者至少,它会工作,但顺序都搞砸了。我当然可以一个大的十进制数,在这里运行它,然后按字段偏移量进行匹配,使其与我的测试相匹配,但这似乎是错误的——另外,我试过的每个小数中至少有三个零字节:) 也许我最后只是偷懒了……毕竟已经晚了。

[StructLayout(LayoutKind.Explicit)]
private readonly struct convert_decimal_to_bytes
{
    [FieldOffset(0)] public readonly decimal value;

    [FieldOffset( 0)] public readonly byte byte_1;
    [FieldOffset( 1)] public readonly byte byte_2;
    [FieldOffset( 2)] public readonly byte byte_3;
    [FieldOffset( 3)] public readonly byte byte_4;
    [FieldOffset( 4)] public readonly byte byte_5;
    [FieldOffset( 5)] public readonly byte byte_6;
    [FieldOffset( 6)] public readonly byte byte_7;
    [FieldOffset( 7)] public readonly byte byte_8;
    [FieldOffset( 8)] public readonly byte byte_9;
    [FieldOffset( 9)] public readonly byte byte_10;
    [FieldOffset(10)] public readonly byte byte_11;
    [FieldOffset(11)] public readonly byte byte_12;
    [FieldOffset(12)] public readonly byte byte_13;
    [FieldOffset(13)] public readonly byte byte_14;
    [FieldOffset(14)] public readonly byte byte_15;
    [FieldOffset(15)] public readonly byte byte_16;

    public convert_decimal_to_bytes(decimal value)
    {
        byte_1  = default;
        byte_2  = default;
        byte_3  = default;
        byte_4  = default;
        byte_5  = default;
        byte_6  = default;
        byte_7  = default;
        byte_8  = default;
        byte_9  = default;
        byte_10 = default;
        byte_11 = default;
        byte_12 = default;
        byte_13 = default;
        byte_14 = default;
        byte_15 = default;
        byte_16 = default;
        this.value = value;
    }

    public convert_decimal_to_bytes(int startIndex, byte[] buffer)
    {
        value = default;
        byte_1 = buffer[startIndex++];
        byte_2 = buffer[startIndex++];
        byte_3 = buffer[startIndex++];
        byte_4 = buffer[startIndex++];
        byte_5 = buffer[startIndex++];
        byte_6 = buffer[startIndex++];
        byte_7 = buffer[startIndex++];
        byte_8 = buffer[startIndex++];
        byte_9 = buffer[startIndex++];
        byte_10 = buffer[startIndex++];
        byte_11 = buffer[startIndex++];
        byte_12 = buffer[startIndex++];
        byte_13 = buffer[startIndex++];
        byte_14 = buffer[startIndex++];
        byte_15 = buffer[startIndex++];
        byte_16 = buffer[startIndex];
    }


    public static void Copy(decimal value, int startIndex, byte[] buffer)
    {
        var convert = new convert_decimal_to_bytes(value);

        buffer[startIndex++] = convert.byte_1;
        buffer[startIndex++] = convert.byte_2;
        buffer[startIndex++] = convert.byte_3;
        buffer[startIndex++] = convert.byte_4;
        buffer[startIndex++] = convert.byte_5;
        buffer[startIndex++] = convert.byte_6;
        buffer[startIndex++] = convert.byte_7;
        buffer[startIndex++] = convert.byte_8;
        buffer[startIndex++] = convert.byte_9;
        buffer[startIndex++] = convert.byte_10;
        buffer[startIndex++] = convert.byte_11;
        buffer[startIndex++] = convert.byte_12;
        buffer[startIndex++] = convert.byte_13;
        buffer[startIndex++] = convert.byte_14;
        buffer[startIndex++] = convert.byte_15;
        buffer[startIndex]   = convert.byte_16;
    }

    public static decimal Read(int startIndex, byte[] buffer)
    {
        var convert = new convert_decimal_to_bytes(startIndex, buffer);

        return convert.value;
    }
}

【问题讨论】:

  • 字节数组肯定需要分配。您可能的意思是该方法应该将decimals 内容传输到外部提供的缓冲区,或者? decimal.GetBits(Decimal, Span<Int32>) 是否符合您的需求?
  • 当然缓冲区需要分配。我不认为这是模棱两可的。缓冲区总是在方法之外。我认为这不值得投反对票。我将证明我正在寻找的样本。 decimal.GetBits 分配 int[4] - 所以不,它显然不符合我的需要。 ——为什么投反对票?我添加了一个示例方法签名,因此希望这可以消除歧义。
  • @mhand:您提到了返回 int[]decimal.GetBits 方法,但 Klaus 明确 提到了接受 Span&lt;int&gt; 的重载,而 确实如此 满足您的需求(如我的回答所示)。在拒绝那些试图帮助你的人的建议之前,请小心。
  • 我还强烈建议即使对于一次性示例代码也遵循正常的 C# 和 .NET 命名约定 - 所有那些非常规的下划线(以及以小写字母开头的类型名称)都可能会让人分心。
  • 您可能对这篇文章感兴趣:Fun with __makeref。滚动到标题为“用例:重新解释变量”的部分。

标签: c#


【解决方案1】:

在 .NET 5.0 之前,如果没有一些丑陋的骇客,这是很尴尬的。从 .NET 5.0 开始,接受 span 的方法越来越多。

您可以使用堆栈分配跨度的GetBits(decimal d, Span&lt;int&gt;) 方法,然后根据需要将四个整数转换为现有字节数组,例如BitConverter.TryWriteBytes

在另一个方向,有一个Decimal(ReadOnlySpan&lt;int&gt;) 构造函数,因此您可以再次stackalloc 一个Span&lt;int&gt;,重复使用BitConverter.ToInt32(ReadOnlySpan&lt;byte&gt;) 从字节数组填充该跨度,并将其传递给构造函数。

顺便说一句,您可能希望通过代码库更广泛地采用跨度,而不是接受字节数组和起始索引。

这里有一些示例代码可以完成上述所有操作 - 可能会稍微更有效地完成,但希望这可以让想法得到理解,并且确实避免了分配:

using System;

class Program
{
    public static void Copy(decimal value, int startIndex, byte[] buffer)
    {
        Span<int> int32s = stackalloc int[4];
        decimal.GetBits(value, int32s);

        var bufferSpan = buffer.AsSpan();
        for (int i = 0; i < 4; i++)
        {
            // These slices are bigger than we need, but this is the simplest approach.
            var slice = bufferSpan.Slice(startIndex + i * 4);
            if (!BitConverter.TryWriteBytes(slice, int32s[i]))
            {
                throw new ArgumentException("Not enough space in span");
            }
        }
    }

    public static decimal Read(int startIndex, byte[] buffer)
    {
        Span<int> int32s = stackalloc int[4];
        ReadOnlySpan<byte> bufferSpan = buffer.AsSpan();
        for (int i = 0; i < 4; i++)
        {
            var slice = bufferSpan.Slice(startIndex + i * 4);
            int32s[i] = BitConverter.ToInt32(slice);
        }
        return new decimal(int32s);
    }

    static void Main()
    {
        byte[] bytes = new byte[16];
        decimal original = 1234.567m;
        Copy(original, 0, bytes);
        decimal restored = Read(0, bytes);
        Console.WriteLine(restored);
    }
}

或同样的事情使用MemoryMarshal:

public static void Copy(decimal value, int startIndex, byte[] buffer)
    => decimal.GetBits(value, MemoryMarshal.Cast<byte, int>(buffer.AsSpan(startIndex)));

public static decimal Read(int startIndex, byte[] buffer)
    => new decimal(MemoryMarshal.Cast<byte, int>(buffer.AsSpan(startIndex)));

【讨论】:

  • 需要注意的是,这只适用于.NET 5.0
  • @KlausGütter 我只是在查找 - 看来我终于可以开始 long 停用这个 hackola 的过程了:i.stack.imgur.com/fzKLM.png
  • 为了 OP 的利益:如果你不在 .NET 5 上,这里是上面所有可疑荣耀中的黑客:github.com/protobuf-net/protobuf-net/blob/main/src/… - 查看DecimalAccessor 的所有用法
  • 我的主要目标是出于 GC 原因删除堆分配,此代码位于非常重/低延迟的数据馈送中。现在有了选项,我将不得不对每个选项进行 bench.net。感谢您的所有意见!
  • 建议改进而不是偏移片加位转换器:MemoryMarshal.Cast&lt;byte, int&gt;(和&lt;int, byte&gt;) - 更直接并允许更简单的循环;警告:任何直接使用byte 数据的方法(包括此答案中显示的代码,以及通过MemoryMarshal.Cast)都可能需要考虑字节顺序
【解决方案2】:

事实上,查阅 MSDN 示例代码,我找到了一个简单的方法来做到这一点:

        int[] data = decimal.GetBits(h);
        bool sign = (data[3] & 0x80000000) != 0;
        byte scale = (byte)(((data[3] >> 16) & 0x7F) + (byte)(!sign ? 0x20 : 0x00));
        List<byte> bytes = new List<byte>() // For concatenation
        bytes.AddRange(BitConverter.GetBytes(data[0]));
        bytes.AddRange(BitConverter.GetBytes(data[1]));
        bytes.AddRange(BitConverter.GetBytes(data[2]));
        bytes.Add(scale);
        return bytes.ToArray();

然后返回:

        byte[] bytes = array.Take(13);
        byte scale = bytes[12];
        bool sign = true;
        if (scale >= 0x20)
        {
            sign = false;
            scale -= 0x20;
        }

        return new decimal(BitConverter.ToInt32(bytes,0), 
                           BitConverter.ToInt32(bytes,4), 
                           BitConverter.ToInt32(bytes,8), sign, scale);

我正在使用上面的代码来序列化/反序列化小数。由于 MSDN 文档点是最后一个 int 中未使用的位(前 16 个和最后 3 个,我将最后一个 int 转换为“有符号比例”,所以我有更少的字节要分配。原始十进制大小为 16 -字节长。

【讨论】:

    猜你喜欢
    • 2015-08-08
    • 2013-02-18
    • 1970-01-01
    • 2018-02-28
    • 2018-05-17
    • 2023-03-14
    • 2017-09-13
    • 2023-03-26
    • 2015-01-15
    相关资源
    最近更新 更多