为什么“十进制”数据类型不闪烁?

时间:2021-10-05 16:12:51

GCHandle.Alloc refuses to pin down an array with structs containing the "decimal" data type, but at the same time works fine with "double". What's the reason for that and can I work around it some way?

GCHandle.Alloc拒绝使用包含“十进制”数据类型的结构来固定数组,但同时使用“double”工作正常。这是什么原因,我能以某种方式解决它吗?

I know I can instead use unsafe/fixed to get at pointer to the array, but that won't work with generics. :-(

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

Full sample code to demonstrate the problem. The first Alloc works, but the second fails with a

完整示例代码以演示此问题。第一个Alloc工作,但第二个失败了

Object contains non-primitive or non-blittable data.

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

    public struct X1
    {
        public double X;
    }

    public struct X2
    {
        public decimal X;
    }

Now try this:

现在试试这个:

        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

4 个解决方案

#1


  var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned);

The runtime makes a hard assumption that you are going to call handle.AddrOfPinnedObject(). Surely you are, very little reason to allocated a pinning handle otherwise. That returns an unmanaged pointer, an IntPtr in C#. Distinct from a managed pointer, the kind you get with the fixed keyword.

运行时会假设您要调用handle.AddrOfPinnedObject()。当然,你没有理由分配钉扎手柄。返回一个非托管指针,一个C#中的IntPtr。与托管指针不同,使用fixed关键字获得的类型。

It furthermore assumes that you are going to pass this pointer to code that cares about the value size and representation. But otherwise being unable to inject a conversion, that code is going to party on the IntPtr directly. This requires the value type to be blittable, a geeky word that means that the bytes in the value can simply be interpreted or copied directly without any conversion and with decent odds that the code that uses the IntPtr is going to be able to recognize the value correctly.

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

That's a problem with some .NET types, the bool type is notorious for example. Just try this same code with bool instead of decimal and notice you'll get the exact same exception. System.Boolean is a very difficult interop type, there's no dominant standard that describes what it should look like. It is 4 bytes in the C language and the Winapi, 2 bytes in COM Automation, 1 byte in C++ and several other languages. In other words, the odds that the "other code" is going to have a shot at interpreting the 1 byte .NET value are rather slim. The unpredictable size is especially nasty, that throws off all subsequent members.

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

Much the same with System.Decimal, there is no widely adopted standard that nails down its internal format. Many languages have no support for it at all, notably C and C++ and if you write code in such a language then you need to use a library. Which might use IEEE 754-2008 decimals but that's a johnny-come-lately and suffers from the "too many standards" problem. At the time the CLI spec was written, IEEE 854-1987 standard was around but it was widely ignored. Still a problem today, there are very few processor designs around that support decimals, I only know of PowerPC.

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

Long story short, you need to create your own blittable type to store decimals. The .NET designers decided to use the COM Automation Currency type to implement System.Decimal, the dominant implementation back then thanks to Visual Basic. This is extremely unlikely to change, way too much code around that takes a dependency on the internal format, making this code the most likely to be compatible and fast:

简而言之,您需要创建自己的blittable类型来存储小数。 .NET设计者决定使用COM Automation Currency类型来实现System.Decimal,这是当时的主流实现,这要归功于Visual Basic。这种情况极不可能改变,因为过多的代码依赖于内部格式,使得这些代码最容易兼容和快速:

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

You could also consider uint[] and Decimal.Get/SetBits() but I think it is unlikely it will be faster, you'd have to try.

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

#2


If you love hacks (and only if you love hack) (but note that this should work)

如果你喜欢黑客(只有你喜欢黑客)(但请注意这应该有效)

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

and then:

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

I'm using two quite famous hacks at the same time: using the [StructLayout(LayoutKind.Explicit)] you can superimpose two value types like a C union and you can even superimpose two value type arrays. The last one has a problem: the Length isn't "recalculated", so if you superimpose a byte[] with a long[], if you put there an array of 8 bytes, both fields will show a Length of 8, but clearly if you try to access the long[1] the program will crash. In this case this isn't a problem, because both structures have the same sizeof.

我同时使用两个非常着名的黑客:使用[StructLayout(LayoutKind.Explicit)]你可以叠加两个值类型,如C union,你甚至可以叠加两个值类型数组。最后一个有问题:长度不是“重新计算”,所以如果你用long []叠加一个byte [],如果你把一个8字节的数组放在那里,两个字段都会显示长度为8,但是很明显,如果你试图访问long [1],程序将崩溃。在这种情况下,这不是问题,因为两个结构具有相同的大小。

Note that I've used 4xuint, but I could have used 2xulong, or 16xbyte.

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

#3


Blittable types do not require conversion when they are passed between managed and unmanaged code. MSDN

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

This is not the case for Decimal. How will other app or whoever is consuming your data will be able to understand Decimal structure? Workaround is to break down decimal into 2 integers, 1 for digits, 1 for decimal base, for example 12.34 into 1234 and 2 meaning (1234 / 10^2).

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

To properly convert Decimal to binary use GetBits, opposite operations is a bit tricky, this page has example.

要将Decimal正确转换为二进制使用GetBits,相反的操作有点棘手,此页面有示例。

#4


This piece of code (refactored from another SO question, which I cannot find now) works just fine with decimal[]. Your problem is that decimal is blittable, but not primitive and a struct containing non-primitive type is not pinnable (GCHandle.Alloc shows an error "Object contains non-primitive or non-blittable data.").

这段代码(从另一个SO问题重构,我现在找不到)与decimal []一起正常工作。你的问题是十进制是blittable,但不是原始的,并且包含非基本类型的结构不是可固定的(GCHandle.Alloc显示错误“对象包含非原始或非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;
    }
}

#1


  var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned);

The runtime makes a hard assumption that you are going to call handle.AddrOfPinnedObject(). Surely you are, very little reason to allocated a pinning handle otherwise. That returns an unmanaged pointer, an IntPtr in C#. Distinct from a managed pointer, the kind you get with the fixed keyword.

运行时会假设您要调用handle.AddrOfPinnedObject()。当然,你没有理由分配钉扎手柄。返回一个非托管指针,一个C#中的IntPtr。与托管指针不同,使用fixed关键字获得的类型。

It furthermore assumes that you are going to pass this pointer to code that cares about the value size and representation. But otherwise being unable to inject a conversion, that code is going to party on the IntPtr directly. This requires the value type to be blittable, a geeky word that means that the bytes in the value can simply be interpreted or copied directly without any conversion and with decent odds that the code that uses the IntPtr is going to be able to recognize the value correctly.

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

That's a problem with some .NET types, the bool type is notorious for example. Just try this same code with bool instead of decimal and notice you'll get the exact same exception. System.Boolean is a very difficult interop type, there's no dominant standard that describes what it should look like. It is 4 bytes in the C language and the Winapi, 2 bytes in COM Automation, 1 byte in C++ and several other languages. In other words, the odds that the "other code" is going to have a shot at interpreting the 1 byte .NET value are rather slim. The unpredictable size is especially nasty, that throws off all subsequent members.

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

Much the same with System.Decimal, there is no widely adopted standard that nails down its internal format. Many languages have no support for it at all, notably C and C++ and if you write code in such a language then you need to use a library. Which might use IEEE 754-2008 decimals but that's a johnny-come-lately and suffers from the "too many standards" problem. At the time the CLI spec was written, IEEE 854-1987 standard was around but it was widely ignored. Still a problem today, there are very few processor designs around that support decimals, I only know of PowerPC.

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

Long story short, you need to create your own blittable type to store decimals. The .NET designers decided to use the COM Automation Currency type to implement System.Decimal, the dominant implementation back then thanks to Visual Basic. This is extremely unlikely to change, way too much code around that takes a dependency on the internal format, making this code the most likely to be compatible and fast:

简而言之,您需要创建自己的blittable类型来存储小数。 .NET设计者决定使用COM Automation Currency类型来实现System.Decimal,这是当时的主流实现,这要归功于Visual Basic。这种情况极不可能改变,因为过多的代码依赖于内部格式,使得这些代码最容易兼容和快速:

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

You could also consider uint[] and Decimal.Get/SetBits() but I think it is unlikely it will be faster, you'd have to try.

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

#2


If you love hacks (and only if you love hack) (but note that this should work)

如果你喜欢黑客(只有你喜欢黑客)(但请注意这应该有效)

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

and then:

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

I'm using two quite famous hacks at the same time: using the [StructLayout(LayoutKind.Explicit)] you can superimpose two value types like a C union and you can even superimpose two value type arrays. The last one has a problem: the Length isn't "recalculated", so if you superimpose a byte[] with a long[], if you put there an array of 8 bytes, both fields will show a Length of 8, but clearly if you try to access the long[1] the program will crash. In this case this isn't a problem, because both structures have the same sizeof.

我同时使用两个非常着名的黑客:使用[StructLayout(LayoutKind.Explicit)]你可以叠加两个值类型,如C union,你甚至可以叠加两个值类型数组。最后一个有问题:长度不是“重新计算”,所以如果你用long []叠加一个byte [],如果你把一个8字节的数组放在那里,两个字段都会显示长度为8,但是很明显,如果你试图访问long [1],程序将崩溃。在这种情况下,这不是问题,因为两个结构具有相同的大小。

Note that I've used 4xuint, but I could have used 2xulong, or 16xbyte.

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

#3


Blittable types do not require conversion when they are passed between managed and unmanaged code. MSDN

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

This is not the case for Decimal. How will other app or whoever is consuming your data will be able to understand Decimal structure? Workaround is to break down decimal into 2 integers, 1 for digits, 1 for decimal base, for example 12.34 into 1234 and 2 meaning (1234 / 10^2).

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

To properly convert Decimal to binary use GetBits, opposite operations is a bit tricky, this page has example.

要将Decimal正确转换为二进制使用GetBits,相反的操作有点棘手,此页面有示例。

#4


This piece of code (refactored from another SO question, which I cannot find now) works just fine with decimal[]. Your problem is that decimal is blittable, but not primitive and a struct containing non-primitive type is not pinnable (GCHandle.Alloc shows an error "Object contains non-primitive or non-blittable data.").

这段代码(从另一个SO问题重构,我现在找不到)与decimal []一起正常工作。你的问题是十进制是blittable,但不是原始的,并且包含非基本类型的结构不是可固定的(GCHandle.Alloc显示错误“对象包含非原始或非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;
    }
}