检查一个类型是否可按位复制的最快方法是什么?

20

在我的序列化器/反序列化器中,我有以下代码片段:

    if (element_type.IsValueType && collection_type.IsArray)
    {
        try
        {
            GCHandle h = GCHandle.Alloc(array_object, GCHandleType.Pinned);
            int arrayDataSize = Marshal.SizeOf(element_type) * c.Count;
            var array_data = new byte[arrayDataSize];
            Marshal.Copy(h.AddrOfPinnedObject(), array_data, 0, arrayDataSize);
            h.Free();
            WriteByteArray(array_data);

            return;
        }
        catch (ArgumentException)
        {
            //if the value type is not blittable, then we need to serialise each array item one at a time
        }
    }

该方法的目的是尝试以最高效的方式(即仅作为一堆字节的内容)将值类型数组写入流中。

问题出现在类型是值类型但不可平坦化时,Alloc() 就会失败。目前,捕获异常并将控制传递给处理数组的代码,就好像它由引用类型组成。

然而,这种检查(由于抛出和捕获异常非常缓慢)由于应用程序中遇到的值类型数量众多,正在证明是一个严重的瓶颈。因此,我想知道检查类型是否可平坦化的最快方法是什么?


我曾经遇到过同样的问题,最终我为每种类型缓存了结果(例如在静态字典中)。检查方式与此处相同,采用try/catch。 - Ondrej Petrzilka
9个回答

11

目前的答案适用于提问者的情况,但是根据规范,可托管值类型数组本身也是可托管类型。稍微扩展了Ondřej的方法,因此它考虑到了这一点,并且也适用于引用类型:

public static bool IsBlittable<T>()
{
    return IsBlittableCache<T>.Value;
}

public static bool IsBlittable(Type type)
{
    if(type.IsArray)
    {
        var elem = type.GetElementType();
        return elem.IsValueType && IsBlittable(elem);
    }
    try{
        object instance = FormatterServices.GetUninitializedObject(type);
        GCHandle.Alloc(instance, GCHandleType.Pinned).Free();
        return true;
    }catch{
        return false;
    }
}

private static class IsBlittableCache<T>
{
    public static readonly bool Value = IsBlittable(typeof(T));
}

作为一个副作用,这将返回(尽管正确)false,因为GetUninitializedObject不能创建string。假设Alloc确实检查可移植性(除了string),那么这应该是可靠的。

这将返回 false,即使 int[] 是可平坦化的。删除 !elem.IsValueType 中的 NOT 即可修复 :) - FooBarTheLittle
@FooBarTheLittle 谢谢! - IS4
@IllidanS4supportsMonica:这种方法无法检测已进行编组设置的结构,例如StructLayout(LayoutKind.Sequential),以及每个字段上的MarshalAs()属性。另一方面,一个涉及 Marshal.SizeOf() 的测试、使用任意数量的技巧创建该大小的非托管缓冲区,然后检查 Marshal.PtrToStructure() 是否成功的方法呢?你认为呢? - ulatekh
@ulatekh Blittable并不意味着可进行封送处理。您在首先设置字段上的“MarshalAs”表明这样的结构体无法进行blittable操作。 - IS4
@IllidanS4supportsMonica:好的,我理解了...我想我的需求可能稍有不同。感谢您的澄清。 - ulatekh

8

我正在使用通用类来缓存结果。测试以相同的方式进行(尝试分配固定句柄)。

public static class BlittableHelper<T>
{
    public static readonly bool IsBlittable;

    static BlittableHelper()
    {
        try
        {
            // Class test
            if (default(T) != null)
            {
                // Non-blittable types cannot allocate pinned handle
                GCHandle.Alloc(default(T), GCHandleType.Pinned).Free();
                IsBlittable = true;
            }
        }
        catch { }
    }
}

缓存是我最终采取的方法,尽管我认为你在这里使用的缓存技术是我见过的最有效的! - sebf
1
请注意,这个程序在Mono上无法工作,因为GCHandle.Alloc对于非blittable类型不会抛出异常。请参考https://github.com/mono/mono/pull/4533。 - Jay Lemmon
2
@JayLemmon 如果你在使用Unity,可以使用UnsafeUtility.IsBlittable。否则,你可能需要递归“遍历字段”。 - Ondrej Petrzilka
这意味着 int[] 不可 Blittable,尽管 https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types 明确表示整数的一维数组是可 Blittable 的。我在这里漏掉了什么,或者那个 default(T) != null 检查需要被移除吗?(根据同一参考,只要视情况进行编组,仅包含可 Blittable 成员的类也可以是可 Blittable 的。) - Matt Tsōnto
1
@MattTsōnto int数组的内容是可平坦化的,但是对数组本身的引用(存储在int[]变量中)不可平坦化。 - Ondrej Petrzilka

4
这个页面上由@IllidanS4编写的优秀代码在处理元素为可平坦化格式化类型的数组时错误地返回false,这意味着该数组也是可平坦化的。在那个例子的基础上,我解决了这个问题,并添加了对一些其他被处理不当的情况的处理,例如:
  • T[]其中T:格式化类型(刚刚提到)
  • 锯齿形数组int[][][]...
  • 枚举(但不是System.Enum本身)
  • 接口、抽象类型
  • 泛型类型(永远不可平坦化)。

我还增加了避免昂贵的异常块的情况,并为我能想到的所有不同类型运行了单元测试。

public static bool IsBlittable(this Type T)
{
    while (T.IsArray)
        T = T.GetElementType();

    bool b;
    if (!((b = T.IsPrimitive || T.IsEnum) || T.IsAbstract || T.IsAutoLayout || T.IsGenericType))
        try
        {
            GCHandle.Alloc(FormatterServices.GetUninitializedObject(T), GCHandleType.Pinned).Free();
            b = true;
        }
        catch { }
    return b;
}

其他答案中的良好缓存机制应该按原样使用。

1
检查其他类型的想法很好。只有一个小错误,boolchar虽然是原始类型,但不是可平坦化的(大小取决于平台)。另外,交错数组不应该是可平坦化的,因为它们是对象引用的数组。根据MSDN的说法,多维数组也不是可平坦化的,尽管我的代码存在相同的问题。 - IS4

1

我没有足够的声望来添加评论,所以我会将我的评论写成答案:

我已经测试了@IS4提出的代码,他的函数说一个字符串是不可复制的,这是正确的。然而,在Unity中使用Mono后端时,他的实现也表示一个带有字符串字段的结构体是可复制的(这是不正确的)。

我还测试了Unity的UnsafeUtility.IsBlittable()函数,并且它对这些结构体返回了正确的值,因此如果我们想要实现一个在Mono上正确工作的IsBlittable()函数,我认为我们别无选择,只能使用反射来确保结构体中的所有字段都是可复制的。

我已经在Unity 2017.4和Unity 2018.4中使用了Mono脚本后端测试了这个实现,到目前为止,它似乎与我尝试过的所有类型都可以正常工作:

using System;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.InteropServices;

public static class BlittableHelper
{
#if UNITY_2018_1_OR_NEWER || UNITY_2019_1_OR_NEWER || UNITY_2020_1_OR_NEWER
    // If we're using Unity, the simplest solution is using
    // the built-in function
    public static bool IsBlittableType(Type type)
    {
        return Unity.Collections.LowLevel.Unsafe.UnsafeUtility.IsBlittable(
            type
        );
    }
#else
    // NOTE: static properties are not taken into account when
    // deciding whether a type is blittable, so we only need
    // to check the instance fields and properties.
    private static BindingFlags Flags =
    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

    public static bool IsBlittableType(Type type)
    {
        // According to the MSDN, one-dimensional arrays of blittable
        // primitive types are blittable.
        if (type.IsArray)
        {
            // NOTE: we need to check if elem.IsValueType because
            // multi-dimensional (jagged) arrays are not blittable.
            var elem = type.GetElementType();
            return elem.IsValueType && IsBlittableType(elem);
        }

        // These are the cases which the MSDN states explicitly
        // as blittable.
        if
        (
            type.IsEnum
            || type == typeof(Byte)
            || type == typeof(SByte)
            || type == typeof(Int16)
            || type == typeof(UInt16)
            || type == typeof(Int32)
            || type == typeof(UInt32)
            || type == typeof(Int64)
            || type == typeof(UInt64)
            || type == typeof(IntPtr)
            || type == typeof(UIntPtr)
            || type == typeof(Single)
            || type == typeof(Double)
        )
        {
            return true;
        }


        // These are the cases which the MSDN states explicitly
        // as not blittable.
        if
        (
            type.IsAbstract
            || type.IsAutoLayout
            || type.IsGenericType
            || type == typeof(Array)
            || type == typeof(Boolean)
            || type == typeof(Char)
            //|| type == typeof(System.Class)
            || type == typeof(Object)
            //|| type == typeof(System.Mdarray)
            || type == typeof(String)
            || type == typeof(ValueType)
            || type == typeof(Array)
            //|| type == typeof(System.Szarray)
        )
        {
            return false;
        }


        // If we've reached this point, we're dealing with a complex type
        // which is potentially blittable.
        try
        {
            // Non-blittable types are supposed to throw an exception,
            // but that doesn't happen on Mono.
            GCHandle.Alloc(
                FormatterServices.GetUninitializedObject(type),
                GCHandleType.Pinned
            ).Free();

            // So we need to examine the instance properties and fields
            // to check if the type contains any not blittable member.
            foreach (var f in type.GetFields(Flags))
            {
                if (!IsBlittableTypeInStruct(f.FieldType))
                {
                    return false;
                }
            }

            foreach (var p in type.GetProperties(Flags))
            {
                if (!IsBlittableTypeInStruct(p.PropertyType))
                {
                    return false;
                }
            }

            return true;
        }
        catch
        {
            return false;
        }
    }

    private static bool IsBlittableTypeInStruct(Type type)
    {
        if (type.IsArray)
        {
            // NOTE: we need to check if elem.IsValueType because
            // multi-dimensional (jagged) arrays are not blittable.
            var elem = type.GetElementType();
            if (!elem.IsValueType || !IsBlittableTypeInStruct(elem))
            {
                return false;
            }

            // According to the MSDN, a type that contains a variable array
            // of blittable types is not itself blittable. In other words:
            // the array of blittable types must have a fixed size.
            var property = type.GetProperty("IsFixedSize", Flags);
            if (property == null || !(bool)property.GetValue(type))
            {
                return false;
            }
        }
        else if (!type.IsValueType || !IsBlittableType(type))
        {
            // A type can be blittable only if all its instance fields and
            // properties are also blittable.
            return false;
        }

        return true;
    }
#endif
}

// This class is used as a caching mechanism to improve performance.
public static class BlittableHelper<T>
{
    public static readonly bool IsBlittable;

    static BlittableHelper()
    {
        IsBlittable = BlittableHelper.IsBlittableType(typeof(T));
    }
}

1
这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - Terru_theTerror
抱歉,这是我在该网站的第一次贡献。我花了一些时间进行更多的测试,以便提供一个更有用的答案。 - David Gutiérrez Palma

1
netcore2.0开始,有System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences<T>,可以让您检查类型是否可平坦化。
static bool IsBlittable<T>()
   => !RuntimeHelpers.IsReferenceOrContainsReferences<T>();

static bool IsBlittable(Type type)
{
    return (bool)typeof(RuntimeHelpers)
               .GetMethod(nameof(RuntimeHelpers.IsReferenceOrContainsReferences))
               .MakeGenericMethod(type)
               .Invoke(null, null);
}

我使用这个实现来发送数组到网络。
ValueTask SendAsync<T>(T[] array, CancellationToken token) where T : unmanaged
{
     // zero allocations, no <AllowUnsafeBlocks> required
     return _stream.WriteAsync(MemoryMarshal.AsBytes((ReadOnlySpan<T>)array, token);
}

Unmanaged 约束强制使用 blittable 类型。参考资料


这会导致不正确的结果。例如,它声称 bool 是可平坦化的,而 int[] 则不是。 - Matt Tsōnto
1
@MattTsōnto,OP正在寻找一种将通用数据写入流的方法,而不是COM互操作。因此,我认为这在序列化方案中是正确的(就C#F#所发现的而言),但不适用于COM互操作。也许术语blittable不是正确的术语。 - JL0PD
@JL0PD:OP希望在使用GCHandle.Alloc时避免异常,即使是完全非托管但仍然是非平凡类型,例如boolcharDateTimedecimal等。这与COM互操作无关。问题不在于如何检查值类型是否可以安全序列化,而在于GCHandle.Alloc拒绝固定某些非平凡对象,即使它们可以安全地序列化。 - György Kőszeg

0

这里有一个替代方案,它只是Microsoft文档所说的直接表示。它不是很短,但比其他解决方案更正确地处理了更多情况。如果您担心反射调用的性能,可以将其包装在简单的缓存中。

static bool IsBlittable(Type type)
    => IsBlittablePrimitive(type)
    || IsBlittableArray(type)
    || IsBlittableStruct(type)
    || IsBlittableClass(type);
static bool IsBlittablePrimitive(Type type)
    => type == typeof(byte)
    || type == typeof(sbyte)
    || type == typeof(short)
    || type == typeof(ushort)
    || type == typeof(int)
    || type == typeof(uint)
    || type == typeof(long)
    || type == typeof(ulong)
    || type == typeof(System.IntPtr)
    || type == typeof(System.UIntPtr)
    || type == typeof(float)
    || type == typeof(double)
    ;
static bool IsBlittableArray(Type type)
    => type.IsArray
    && type.GetArrayRank() == 1
    && IsBlittablePrimitive(type.GetElementType())
    ;
static bool IsBlittableStruct(Type type)
    => type.IsValueType
    && !type.IsPrimitive
    && type.IsLayoutSequential
    && type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).All(IsBlittableField);
static bool IsBlittableClass(Type type)
    => !type.IsValueType
    && !type.IsPrimitive
    && type.IsLayoutSequential
    && type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).All(IsBlittableField);
static bool IsBlittableField(FieldInfo field)
    => IsBlittablePrimitive(field.FieldType) 
    || IsBlittableStruct(field.FieldType);

测试用例:

Is blittable?
- Int32: True
- Int32[]: True
- Int32[,]: False
- Int32[][]: False
- String: False
- String[]: False
- Boolean: False
- String: False
- Byte[]: True
- struct X { public int x; }: True
- struct Y { public int[] Foo { get; set; } }: False
- class CAuto { public int X { get; set; } }: False
- [StructLayout(LayoutKind.Sequential)]class CSeq { public int X { get; set; } }: True

注意:这个报告将Span视为可位移类型,但我不能确定是否正确。

0

最快的方法是不分配,而是重用现有的GCHandle,例如:

var gch = GCHandle.Alloc(null, GCHandleType.Pinned);
gch.Target = new byte[0];
gch.Target = "";

GCHandle.Alloc 每次分配或重用现有插槽时都会进行锁定,这是相对昂贵的操作。当 JIT 编译时,静态只读基元类型变成常量,但不要在泛型类型中存储 GCHandle,因为每个泛型实例化将取其自己的副本。


0
这对我有效:
static bool IsBlittable(Type t)
{
  if (t.IsPrimitive) return true; if (!t.IsValueType) return false;
  var a = t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
  for (int i = 0; i < a.Length; i++) if (!IsBlittable(a[i].FieldType)) return false;
  return true;
}

这会导致不正确的结果。例如,它说 bool 是可平坦化的,而 int[] 不是。 - Matt Tsōnto

0

2
谢谢,但不幸的是这并不起作用。我尝试了至少一个非平坦类型(一个简单的带有字符串的结构体),IsLayoutSequential属性为真。 - sebf

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