为什么byte[]的最大大小是2 GB - 57 B?

32

在我的64位机器上,这段C#代码可以工作:

new byte[2L * 1024 * 1024 * 1024 - 57]

但是这个会抛出一个OutOfMemoryException异常:

new byte[2L * 1024 * 1024 * 1024 - 56]

为什么?

我知道托管对象的最大大小是2GB,而我创建的数组对象包含的字节数超过了我想要的大小。具体来说,同步块编号需要4个字节(或8个字节),MethodTable引用需要8个字节,数组大小需要4个字节。这总共是24个字节,包括填充字节,那么为什么我不能分配一个大小为2G-24字节的数组呢?难道最大大小确实是恰好2GB吗?如果是这样,那剩余的2GB用于什么?

注:我实际上不需要分配一个大小为2百万字节的数组。即使我需要,56字节的开销也很小。我可以轻松地通过自定义 BigArray<T> 来解决这个限制。


MethodTable 的引用在 64 位上将为 8 字节,除此之外可能还有填充以对齐。 - Brian Rasmussen
整型值的上限可能是多少呢?我想一个 byte[] 数组在给定整数元素索引时不能超过 int 的最大值。 - Brad Christie
2
@svick:一如既往,Jon Skeet对于一个类似的 .net 问题有了答案。(MSDN提到这个上限可以在这里找到:http://msdn.microsoft.com/en-us/library/ms241064%28VS.80%29.aspx) - Brad Christie
1
这将从大对象堆中分配。那个可能直接从Windows的低碎片堆中分配(取决于您的操作系统)。您需要添加Windows内存管理器在LFH中添加到堆块的开销。大小大约正确。 - Hans Passant
1
这里有一个关于.NET数组开销的详细讨论。https://dev59.com/JHI-5IYBdhLWcg3w99oH 我认为这不是特别有趣的讨论,因为它只是一些实现细节。当然,除非你正在编写.NET Framework的实现。 - Can Gencer
显示剩余6条评论
3个回答

20

你需要56字节的开销。实际上是2,147,483,649-1减去56得到的最大大小。这就是为什么你的“减57”有效而“减56”无效。

正如Jon Skeet在这里所说:

然而,在实际情况下,我不相信任何实现支持如此巨大的数组。CLR有一个比2GB稍短的每个对象限制,因此即使是一个字节数组,也不能实际拥有2147483648个元素。一些实验表明,在我的机器上,您可以创建的最大数组是new byte [2147483591]。(这是在64位.NET CLR上;我安装的Mono版本无法处理它。)

请参见InformIT文章中关于同一主题的内容。它提供了以下代码以演示最大大小和开销:

class Program
{
  static void Main(string[] args)
  {
    AllocateMaxSize<byte>();
    AllocateMaxSize<short>();
    AllocateMaxSize<int>();
    AllocateMaxSize<long>();
    AllocateMaxSize<object>();
  }

  const long twogigLimit = ((long)2 * 1024 * 1024 * 1024) - 1;
  static void AllocateMaxSize<T>()
  {
    int twogig = (int)twogigLimit;
    int num;
    Type tt = typeof(T);
    if (tt.IsValueType)
    {
      num = twogig / Marshal.SizeOf(typeof(T));
    }
    else
    {
      num = twogig / IntPtr.Size;
    }

    T[] buff;
    bool success = false;
    do
    {
      try
      {
        buff = new T[num];
        success = true;
      }
      catch (OutOfMemoryException)
      {
        --num;
      }
    } while (!success);
    Console.WriteLine("Maximum size of {0}[] is {1:N0} items.", typeof(T).ToString(), num);
  }
}

最后,文章说:

如果你算一下,你会发现分配一个数组的开销是56字节。由于对象大小,末尾还剩下一些字节。例如,268,435,448个64位数字占用2,147,483,584字节。加上56字节的数组开销,总共是2,147,483,640,离2GB还差7个字节。

编辑:

但等等,还有更多!

四处寻找并与Jon Skeet交谈后,他指向了他写的关于内存和字符串的一篇文章。在那篇文章中,他提供了一个大小表:

Type            x86 size            x64 size
object          12                  24
object[]        16 + length * 4     32 + length * 8
int[]           12 + length * 4     28 + length * 4
byte[]          12 + length         24 + length
string          14 + length * 2     26 + length * 2

斯基特先生接着说:

你看到上面的数字可能会以为在x86中一个对象的“开销”是12个字节,在x64中是24个字节……但实际上不完全是这样。

还有这段话:

  • 在x86中每个对象的“基本”开销为8个字节,在x64中为16个字节……鉴于我们可以在x86中存储一个Int32的“真实”数据并仍然具有12个字节的对象大小,同样地,我们可以在x64中存储两个Int32的真实数据并仍然具有x64的对象。

  • 有一个“最小”大小分别为12字节和24字节。换句话说,您不能只有开销的类型。请注意,“Empty”类占用与创建Object实例相同的大小……实际上有一些多余的空间,因为CLR不喜欢操作没有数据的对象。(请注意,即使对于局部变量,没有字段的结构体也会占用空间。)

  • x86对象填充到4字节边界;在x64上是8字节(就像以前一样)

最后,Jon Skeet在另一个问题中回答了我提出的问题,他在回答中表示(针对我向他展示的InformIT文章):

看起来你所提到的文章仅从限制中推断开销,这在我看来是愚蠢的。

因此,根据我所了解的情况,实际开销为24字节,剩余32字节的空间。


但是为什么是56字节呢?它们里面存储了什么?另外,Marshal.SizeOf()并不能告诉你对象实际的大小,只能告诉你如果你将其marshal化后对象的大小。 - svick
该文档实际上确切地说明了我的意思:“在非托管代码中指定类型的大小。”例如,Marshal.SizeOf(typeof(bool))返回4,但我仍然能够分配new bool[almost2G],这意味着bool的实际大小只有一个字节。 - svick
@svick: 我找到了一篇关于哈希表的不常见文章,可能会有你想要的答案:“16字节的开销,20字节的数组开销,8字节指向数组的指针,每个数组条目的引用占8字节,M占4字节,N占4字节,填充占4字节”(http://algs4.cs.princeton.edu/34hash/)。 - user195488
@svick:我认为因为布尔值是非可插拔类型(non-blittable),所以你是正确的,必须使用 sizeof 而不是 Marshal.sizeof,否则对于字节(byte)来说没问题,除非我漏掉了什么。 - user195488
备用房间?那是什么?为什么我不能把我的字节放在那里? - svick
显示剩余6条评论

3
你可以在 .net 源代码中明确找到并验证这个限制,它提供了一些见解,说明为什么这样做(高级范围检查消除的有效实现): https://github.com/dotnet/runtime/blob/b42188a8143f3c7971a7ab1c735e31d8349e7991/src/coreclr/vm/gchelpers.cpp
inline SIZE_T MaxArrayLength()
{
    // Impose limits on maximum array length to prevent corner case integer overflow bugs
    // Keep in sync with Array.MaxArrayLength in BCL.
    return 0X7FFFFFC7;
}
...

if ((SIZE_T)cElements > MaxArrayLength())
    ThrowOutOfMemoryDimensionsExceeded();

3

可以确定的一件事是,你不能有奇数个字节,通常是本机字大小的倍数,64位进程上的本机字大小为8字节。因此,你可能需要将另外7个字节添加到数组中。


1
通常是本地字大小的倍数。因此,对于64位进程,这将是8个字节。 - svick

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