【问题标题】:Why is "decimal" data type non-blittable?为什么“十进制”数据类型不可位?
【发布时间】:2015-05-13 11:11:54
【问题描述】:

GCHandle.Alloc 拒绝使用包含“decimal”数据类型的结构来确定数组,但同时可以很好地处理“double”。这是什么原因,我可以通过某种方式解决它吗?

我知道我可以改为使用 unsafe/fixed 来获取指向数组的指针,但这不适用于泛型。 :-(

演示问题的完整示例代码。第一个 Alloc 有效,但第二个失败并出现

对象包含非原始数据或非 blittable 数据。

    public struct X1
    {
        public double X;
    }

    public struct X2
    {
        public decimal X;
    }

现在试试这个:

        var x1 = new[] {new X1 {X = 42}};
        var handle1 = GCHandle.Alloc(x1, GCHandleType.Pinned); // Works
        var x2 = new[] { new X2 { X = 42 } };
        var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned); // Fails

【问题讨论】:

  • decimal 与 `double. Double 是本机类型,而 decimal 是 128 位结构
  • 您还可以将 double 视为 64 位结构。
  • 这不是视角问题。 Double 是一种原生类型,如“操作系统和 CPU 原生支持它”。小数虽然“只是”一个内部布局使用 4 个 DWORD 的结构。 OLE 使用相同的布局(实际上就是它的来源),它允许您交换数据,例如与 VB/VBA。 C/C++ 虽然不知道如何处理它
  • 当然你是对的,但在这种情况下,我对数据代表什么或布局完全不感兴趣。我只想要一个指向原始数据的指针。我可以在上面的代码中使用“fixed(void* ptr = x1”)) 来做到这一点。但我想找到一种通用的方法。就这样。结构包含的内容并不重要,只要其中没有引用即可。
  • GCHandle.Alloc 甚至拒绝确定DateTime[]int?[],仅供参考...

标签: c# .net memory-management


【解决方案1】:
  var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned);

运行时很难假设您将调用handle.AddrOfPinnedObject()。当然,您几乎没有理由分配固定句柄。这将返回一个 非托管指针,即 C# 中的 IntPtr。与托管指针不同,后者是使用 fixed 关键字获得的类型。

它还假设您要将此指针传递给关心值大小和表示的代码。但否则无法注入转换,该代码将直接在 IntPtr 上聚会。这要求值类型是 blittable,这是一个令人讨厌的词,这意味着值中的字节可以简单地被解释或直接复制而无需任何转换,并且使用 IntPtr 的代码很有可能正在运行能够正确识别该值。

这是某些 .NET 类型的问题,例如,bool 类型是臭名昭著的。只需使用 bool 而不是 decimal 尝试相同的代码,并注意您将得到完全相同的异常。 System.Boolean 是一种非常困难的互操作类型,没有描述它应该是什么样子的主导标准。在 C 语言和 Winapi 中是 4 个字节,在 COM 自动化中是 2 个字节,在 C++ 和其他几种语言中是 1 个字节。换句话说,“其他代码”将有机会解释 1 字节的 .NET 值的可能性很小。不可预测的大小尤其令人讨厌,它会甩掉所有后续成员。

与 System.Decimal 大致相同,没有广泛采用的标准来确定其内部格式。许多语言根本不支持它,特别是 C 和 C++,如果你用这种语言编写代码,那么你需要使用库。哪个可能使用IEEE 754-2008 小数,但这是最近才出现的,并且受到“标准太多”问题的困扰。在编写 CLI 规范时,IEEE 854-1987 标准已经存在,但被广泛忽略。今天仍然是个问题,支持小数的处理器设计很少,我只知道PowerPC。

长话短说,您需要创建自己的 blittable 类型来存储小数。 .NET 设计者决定使用 COM 自动化 Currency 类型来实现 System.Decimal,这要归功于 Visual Basic。这是极不可能改变的,太多的代码依赖于内部格式,使得这些代码最有可能兼容和快速:

    public struct X2 {
        private long nativeDecimal;
        public decimal X {
            get { return decimal.FromOACurrency(nativeDecimal); }
            set { nativeDecimal = decimal.ToOACurrency(value); }
        }
    }

您也可以考虑 uint[] 和 Decimal.Get/SetBits() 但我认为它不太可能更快,您必须尝试。

【讨论】:

    【解决方案2】:

    如果你喜欢 hack(并且只有你喜欢 hack)(但请注意,这应该有效)

    [StructLayout(LayoutKind.Explicit)]
    public struct DecimalSplitted
    {
        [FieldOffset(0)]
        public uint UInt0;
        [FieldOffset(4)]
        public uint UInt1;
        [FieldOffset(8)]
        public uint UInt2;
        [FieldOffset(12)]
        public uint UInt3;
    }
    
    [StructLayout(LayoutKind.Explicit)]
    public struct DecimalToUint
    {
        [FieldOffset(0)]
        public DecimalSplitted Splitted;
        [FieldOffset(0)]
        public decimal Decimal;
    }
    
    [StructLayout(LayoutKind.Explicit)]
    public struct StructConverter
    {
        [FieldOffset(0)]
        public decimal[] Decimals;
    
        [FieldOffset(0)]
        public DecimalSplitted[] Splitted;
    }
    

    然后:

    var decimals = new decimal[] { 1M, 2M, decimal.MaxValue, decimal.MinValue };
    
    DecimalSplitted[] asUints = new StructConverter { Decimals = decimals }.Splitted;
    
    // Works correctly
    var h = GCHandle.Alloc(asUints, GCHandleType.Pinned);
    
    // But here we don't need it :-)
    h.Free();
    
    for (int i = 0; i < asUints.Length; i++)
    {
        DecimalSplitted ds = new DecimalSplitted
        {
            UInt0 = asUints[i].UInt0,
            UInt1 = asUints[i].UInt1,
            UInt2 = asUints[i].UInt2,
            UInt3 = asUints[i].UInt3,
        };
    
        Console.WriteLine(new DecimalToUint { Splitted = ds }.Decimal);
    }
    

    我同时使用了两个非常著名的技巧:使用[StructLayout(LayoutKind.Explicit)],您可以叠加两个值类型,例如 C 联合 ,您甚至可以叠加两个值类型数组。最后一个有问题:Length 没有“重新计算”,所以如果你将byte[]long[] 叠加,如果你放一个 8 字节的数组,两个字段都会显示一个Length 8,但很明显,如果您尝试访问long[1],程序将崩溃。在这种情况下,这不是问题,因为两个结构都具有相同的sizeof

    请注意,我使用了 4xuint,但我可以使用 2xulong 或 16xbyte

    【讨论】:

    • 嗯,我知道这一切,但这并不是我真正想要的。但我赞成,因为它既酷又有用! :-)
    【解决方案3】:

    Blittable 类型在它们之间传递时不需要转换 托管和非托管代码。 MSDN

    十进制不是这种情况。其他应用程序或正在使用您的数据的人将如何理解十进制结构?解决方法是将十进制分解为 2 个整数,1 代表数字,1 代表十进制基数,例如 12.34 分解为 1234 和 2 含义(1234 / 10^2)。

    要正确地将十进制转换为二进制使用GetBits,相反的操作有点棘手,this page 有例子。

    【讨论】:

    • 解释是正确的,但解决方法不起作用 - 小数需要 四个 32 位整数,因为数字使用 96 位。您可以使用Decimal.GetBits 直接从小数中获取整数数组
    • 没有其他应用会使用这个。我只想将阵列写入/读取磁盘。这比其他序列化方法(如 Protobuf)快得多。我很惊讶小数不起作用。
    • 小数不是原生类型——事实上你需要 128 位来存储它们。使用GetBits 获取数据然后将其存储到磁盘
    • @PanagiotisKanavos 你是对的,但对于较小的 int/long 应该足够了。
    • @Andrey 在这种情况下您不会传输小数,您会将它们转换为具有舍入和缩放错误的不同表示形式。简单地传输 128 位将比转换快得多
    【解决方案4】:

    这段代码(从另一个 SO 问题重构,我现在找不到)在 decimal[] 上工作得很好。您的问题是 decimal 是 blittable,但不是原始的,并且包含非原始类型的结构不可固定(GCHandle.Alloc 显示错误“对象包含 non-primitive 或非 blittable 数据。”)。

    Why is decimal not a primitive type?

    /// <summary>
    /// Helper class for generic array pointers
    /// </summary>
    /// <typeparam name="T"></typeparam>
    internal class GenericArrayPinner<T> : IDisposable
    {
        GCHandle _pinnedArray;
        private T[] _arr;
        public GenericArrayPinner(T[] arr)
        {
            _pinnedArray = GCHandle.Alloc(arr, GCHandleType.Pinned);
            _arr = arr;
        }
        public static implicit operator IntPtr(GenericArrayPinner<T> ap)
        {
    
            return ap._pinnedArray.AddrOfPinnedObject();
        }
    
        /// <summary>
        /// Get unmanaged poinetr to the nth element of generic array
        /// </summary>
        /// <param name="n"></param>
        /// <returns></returns>
        public IntPtr GetNthPointer(int n)
        {
            return Marshal.UnsafeAddrOfPinnedArrayElement(this._arr, n);
        }
    
        public void Dispose()
        {
            _pinnedArray.Free();
            _arr = null;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2020-10-31
      • 1970-01-01
      • 1970-01-01
      • 2018-04-26
      • 2011-03-12
      • 2020-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多