一尘不染

.NET阵列的开销?

c#

我试图使用以下代码确定.NET数组(在32位进程中)标头的开销:

long bytes1 = GC.GetTotalMemory(false);
object[] array = new object[10000];
    for (int i = 0; i < 10000; i++)
        array[i] = new int[1];
long bytes2 = GC.GetTotalMemory(false);
array[0] = null; // ensure no garbage collection before this point

Console.WriteLine(bytes2 - bytes1);
// Calculate array overhead in bytes by subtracting the size of 
// the array elements (40000 for object[10000] and 4 for each 
// array), and dividing by the number of arrays (10001)
Console.WriteLine("Array overhead: {0:0.000}", 
                  ((double)(bytes2 - bytes1) - 40000) / 10001 - 4);
Console.Write("Press any key to continue...");
Console.ReadKey();

结果是

    204800
    Array overhead: 12.478

在32位进程中,object [1]的大小应与int [1]相同,但是实际上开销跳升了3.28个字节,

    237568
    Array overhead: 15.755

有人知道为什么吗?

(顺便说一句,如果有人好奇的话,非数组对象(例如,上面循环中的(object)i)的开销约为8个字节(8.384)。我听说在64位进程中为16个字节。)


阅读 157

收藏
2020-05-19

共1个答案

一尘不染

这是一个稍微整洁的(IMO)简短但完整的程序,用于演示同一件事:

using System;

class Test
{
    const int Size = 100000;

    static void Main()
    {
        object[] array = new object[Size];
        long initialMemory = GC.GetTotalMemory(true);
        for (int i = 0; i < Size; i++)
        {
            array[i] = new string[0];
        }
        long finalMemory = GC.GetTotalMemory(true);
        GC.KeepAlive(array);

        long total = finalMemory - initialMemory;

        Console.WriteLine("Size of each element: {0:0.000} bytes",
                          ((double)total) / Size);
    }
}

但是我得到了相同的结果-
任何引用类型数组的开销都是16个字节,而任何值类型数组的开销都是12个字节。我仍然在CLI规范的帮助下尝试找出原因。不要忘记引用类型数组是协变的,这可能是相关的…

编辑:借助cordbg,我可以确认Brian的答案-
引用类型数组的类型指针是相同的,而不管实际的元素类型是什么。大概是在某种程度上object.GetType()(不是虚拟的,请记住)来解决这个问题。

因此,使用以下代码:

object[] x = new object[1];
string[] y = new string[1];
int[] z = new int[1];
z[0] = 0x12345678;
lock(z) {}

我们最终得到如下内容:

Variables:
x=(0x1f228c8) <System.Object[]>
y=(0x1f228dc) <System.String[]>
z=(0x1f228f0) <System.Int32[]>

Memory:
0x1f228c4: 00000000 003284dc 00000001 00326d54 00000000 // Data for x
0x1f228d8: 00000000 003284dc 00000001 00329134 00000000 // Data for y
0x1f228ec: 00000000 00d443fc 00000001 12345678 // Data for z

请注意,我已经在变量本身的值 之前 转储了1个字。

对于xy,值是:

  • 同步块,用于锁定哈希码(或 瘦锁 -参见Brian的评论)
  • 类型指针
  • 数组大小
  • 元素类型指针
  • 空引用(第一个元素)

对于z,值是:

  • 同步块
  • 类型指针
  • 数组大小
  • 0x12345678(第一个元素)

不同的值类型数组(byte [],int
[]等)最终具有不同的类型指针,而所有引用类型数组都使用相同的类型指针,但具有不同的元素类型指针。元素类型指针的值与该类型对象的类型指针的值相同。因此,如果我们在上面的运行中查看了字符串对象的内存,它的类型指针将为0x00329134。

该类型的指针之前,这个词肯定有 事情
做与液晶显示屏或哈希代码:调用GetHashCode()该位的内存填充,我相信默认object.GetHashCode()获得同步块,以确保哈希代码的唯一性为对象的生命周期。但是,lock(x){}做任何事情都没做,这让我感到惊讶。

顺便说一下,所有这些仅对“向量”类型有效-在CLR中,“向量”类型是下限为0的一维数组。其他数组将具有不同的布局-一方面,他们需要存储下限…

到目前为止,这只是实验,但这只是猜测-系统以其现有方式实施的原因。从这里开始,我真的只是在猜测。

  • 所有object[]阵列可以共享相同的JIT代码。它们将在内存分配,数组访问,Length属性和(重要)GC引用的布局方面表现相同。将其与值类型数组进行比较,在值类型数组中,不同的值类型可能具有不同的GC“足迹”(例如,一个值可能具有一个字节,然后是一个引用,其他值将根本没有引用,等等)。
  • 每次object[]在运行时内分配值时,运行时都需要检查其是否有效。它需要检查用于新元素值的引用的对象类型是否与数组的元素类型兼容。例如:
    object[] x = new object[1];
    

    object[] y = new string[1];
    x[0] = new object(); // Valid
    y[0] = new object(); // Invalid - will throw an exception

这就是我前面提到的协方差。现在,考虑到 每个分配
都会发生这种情况,减少间接数是有意义的。特别是,我怀疑您真的不需要通过每次分配的类型对象都获取元素类型来破坏缓存。我 怀疑
(我的x86程序集不足以验证这一点)测试是否像这样:

  • 要复制的值是否为空引用?如果是这样,那很好。(完成)
  • 获取引用指向的对象的类型指针。
  • 该类型指针与元素类型指针(简单的二进制相等性检查)相同吗?如果是这样,那很好。(完成)
  • 该类型指针分配与元素类型指针兼容吗?(检查要复杂得多,涉及继承和接口。)如果是这样,那很好-否则,抛出异常。

如果我们可以在前三个步骤中终止搜索,则没有太多的间接操作-这对于像数组分配这样频繁发生的事情很有用。对于值类型分配,这些都不需要发生,因为这是静态可验证的。

因此,这就是为什么我认为引用类型数组比值类型数组稍大的原因。

好问题-深入研究真的很有趣:)

2020-05-19