具有通用类型字段的结构体大小

4

我希望估算一个包含泛型类型参数的结构体数组的大小,这种情况下是一个字典条目结构体。为了达到这个目的,我需要知道这个结构体的大小。

struct Entry
{
   int hash;
   int next;
   TKey key;
   TValue value;
}

如何获取此结构体的字节大小?
编辑:
使用 Marshal.SizeOf 似乎存在问题。传递结构体的类型将引发异常,说明参数不能是泛型类型定义。
如果我调用带有实例的重载,例如 Marshal.SizeOf(default(Entry)),则仅当两个泛型类型参数都是值类型时才有效。如果泛型参数为 等,则会抛出此异常:
Dictionary`2+Entry[System.Int32,System.Object]' 无法作为非托管结构进行编组;无法计算任何有意义的大小或偏移量。

这可行吗?毕竟,根据 TKeyTValue 的类型,大小会发生变化。 - George Duckett
如果你使用泛型,编译器基本上会根据不同的TKey/TValue组合创建不同的结构体/类。因此,并没有一个具有固定大小的结构体,而是可能有许多不同的结构体,每个都有自己独立的大小。所以Entry<char, bool>的大小将与Entry<string, decimal>不同。 - Corak
更好的是,Marshal.SizeOf(new Entry<string, decimal>())会抛出ArgumentException异常:"类型“Entry`2[System.String,System.Decimal]”无法作为非托管结构体进行封送;无法计算有意义的大小或偏移量。" - Corak
阅读Jon Skeet的答案:https://dev59.com/LnVC5IYBdhLWcg3wsTjv - Luis Filipe
他建议在数组中分配N个项目,并获取内存使用情况,然后除以N?肯定有更简单的方法吧?此外,他的答案似乎与类实例(而不是结构体)的大小有关,包括字段的大小? - Anders Forsgren
显示剩余3条评论
4个回答

11

听起来,IL sizeof 指令可能是您所需要的。sizeof 指令由 C# sizeof 运算符在幕后使用,但由于某些原因,IL 版本的限制更少。

ECMA CLI 规范(第 III 部分,第 4.25 节)对 sizeof 指令有以下描述:

返回类型的大小(以字节为单位)。typeTok可以是泛型参数、引用类型或值类型。
对于引用类型,返回的大小是相应类型的引用值的大小,而不是由引用值引用的对象中存储的数据的大小。
[解释:值类型的定义在CIL生成时和加载执行时可能会发生变化。因此,在生成CIL时并不总是知道类型的大小。sizeof指令允许CIL代码在运行时确定大小,而无需调用框架类库。计算可以完全在运行时或在CIL到本机代码编译时进行。sizeof返回每个元素在此类型的数组中占用的总大小,包括实现选择添加的任何填充。具体来说,数组元素相距sizeof个字节。]
您应该能够通过一些简单的运行时代码生成获得sizeof指令。
Console.WriteLine("Entry is " + TypeHelper.SizeOf(typeof(Entry)) + " bytes.");

// ...

public static class TypeHelper
{
    public static int SizeOf<T>(T? obj) where T : struct
    {
        if (obj == null) throw new ArgumentNullException("obj");
        return SizeOf(typeof(T?));
    }

    public static int SizeOf<T>(T obj)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        return SizeOf(obj.GetType());
    }

    public static int SizeOf(Type t)
    {
        if (t == null) throw new ArgumentNullException("t");

        return _cache.GetOrAdd(t, t2 =>
            {
                var dm = new DynamicMethod("$", typeof(int), Type.EmptyTypes);
                ILGenerator il = dm.GetILGenerator();
                il.Emit(OpCodes.Sizeof, t2);
                il.Emit(OpCodes.Ret);

                var func = (Func<int>)dm.CreateDelegate(typeof(Func<int>));
                return func();
            });
    }

    private static readonly ConcurrentDictionary<Type, int>
        _cache = new ConcurrentDictionary<Type, int>();
}

请注意,这并不能帮助计算类实例所需的存储空间;它只会给出指向它的引用的大小(在64位执行中为8字节)。 - Oliver Bock

4
近似大小将是hash(4个字节(32位架构))+ next(4个字节(32位架构))+ TKey(如果是引用类型,则为指针的4个字节(32位架构),如果是值类型,则递归计算该值类型的大小)+ TValue(与TKey相同)。
或者,可以简单地使用Marshal.SizeOf方法。

3
指针需要4个字节?现在已经是2013年了,您还假设使用32位的架构吗? - Damien_The_Unbeliever
@Damien_The_Unbeliever:你可能不会相信,但我们公司仍在使用XP系统,我相信也有很多其他公司也是如此。 - Tigran
@Tigran,你能用一个例子展示一下吗?我似乎无法使用sizeof/Marshal.SizeOf与泛型类型一起工作。 - Anders Forsgren
@AndersForsgren:在泛型类型分配之后,您应该能够通过调用var runtimeType = typeof(struct_instance);和SizeOf来生成真实类型。 - Tigran
@AndersForsgren:当然,这就是为什么我建议在结构体的实例上使用typeof,这样您就可以获得实例的运行时类型。 - Tigran
显示剩余3条评论

0
(在我写完这篇文章后,我注意到这种方法已经在LukeH引用的解释中提到了)
struct Pin : IDisposable
{
    public GCHandle pinHandle;
    public Pin(object o) { pinHandle = GCHandle.Alloc(o, GCHandleType.Pinned); }

    public void Dispose()
    {
        pinHandle.Free();
    }
}

static class ElementSize<T>
{
    private static int CalcSize(T[] testarray)
    {
      using (Pin p = new Pin(testarray))
        return (int)(Marshal.UnsafeAddrOfPinnedArrayElement(testarray, 1).ToInt64()
                   - Marshal.UnsafeAddrOfPinnedArrayElement(testarray, 0).ToInt64());
    }

    static public readonly int Bytes = CalcSize(new T[2]);
}

我相信固定和丢弃一个小数组比动态编译更便宜。此外,泛型类中的静态字段是一种很好的方式,可以拥有类型安全的每个类型数据...无需使用ConcurrentDictionary


0

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