【问题标题】:How can I know the ACTUAL maximum number of elements a .net array of a given type can be allocated?我如何知道可以分配给定类型的 .net 数组的实际最大元素数?
【发布时间】:2023-03-06 08:21:01
【问题描述】:

我知道 .net 中的所有数组都限制为 2 GB,在此前提下,我尽量不要在数组中分配更多的 n = ((2^31) - 1) / 8 doubles。尽管如此,这个数量的元素似乎仍然无效。任何人都知道如何在运行时确定给定 sizeof(T) 的最大元素数?

我知道接近这个数字的任何数量都只是很多元素,但出于所有意图和目的,假设我需要它。

注意:我在 64 位环境中,有一个用于我的 AnyCPU 应用程序的目标平台,并且 RAM 中至少有 3100 MB 可用空间。

更新: 谢谢大家的贡献,很抱歉我这么安静。对于给您带来的不便,我深表歉意。我无法重新表述我的问题,但我可以补充一点,我正在寻找的是解决这样的问题:

template <class T>
array<T>^ allocateAnUsableArrayWithTheMostElementsPossible(){
    return gcnew array<T>( ... );
}

我自己回答的结果有点令人满意,但还不够好。此外,我还没有在另一台机器上测试它(很难找到另一台超过 4 GB 的机器)。此外,我一直在自己做一些研究,似乎没有便宜的方法可以在运行时计算它。无论如何,这只是一个优点,what-I-am-trying-to-accomplish 的用户都不能期望在没有能力的情况下使用我尝试实现的功能。

所以,换句话说,我只是想了解为什么数组的最大元素数加起来不等于 2GB其他条件不变。我现在只需要一个最高值。

【问题讨论】:

  • @Abel,感谢您的帮助,我已经重读了我的帖子,但无法重新措辞。我正在寻找的是 .NET 中数组中元素的最大数量。
  • 查找连续字节的数量,或者使用我的代码找到最大可用的非连续字节(尽管它相当昂贵)。将结果与sizeof(T) 整除。我不知道您使用的是 C++.NET,但将 C# 转换为 C++ 应该不会太难。很抱歉,我不理解您自己的答案,也没有看到它与故事的契合度,但我会再试一次。
  • 上一页。评论是关于较早的回答尝试,它解释了如何找到最大可用内存的大小(连续或分散)。在历史中看到这个条目:stackoverflow.com/revisions/1842456/…

标签: .net arrays 64-bit large-object-heap


【解决方案1】:

更新: 完全重写答案。原始答案包含通过分而治之在任何系统上找到最大可能可寻址数组的方法,如果您有兴趣,请参阅此答案的历史。新答案试图解释 56 字节的差距。

his own answer 中,AZ 解释说最大数组大小限制为小于 2GB 上限,并且通过一些试验和错误(或其他方法?)发现以下内容(摘要):

  • 如果类型的大小为1、2、4或8字节,则最大可占用大小为2GB - 56字节;
  • 如果类型的大小为16字节,则最大值为2GB - 48字节;
  • 如果类型的大小为 32 字节,则最大值为 2GB - 32 字节。

我不完全确定 16 字节和 32 字节的情况。如果数组是结构数组或内置类型,则数组的总可用大小可能会有所不同。我将强调 1-8 字节的类型大小(我也不确定,见结论)。

数组的数据布局

要了解为什么 CLR 不完全允许 ​​2GB / IntPtr.Size 元素,我们需要了解数组的结构。一个很好的起点是SO article,但不幸的是,有些信息似乎是错误的,或者至少是不完整的。这in-depth article on how the .NET CLR creates runtime objects 被证明是无价的,还有这篇关于 CodeProject 的Arrays Undocumented 文章。

综合这些文章中的所有信息,可以归结为 32 位系统中数组的以下布局:

单维,内置型 SSSSTTTLLLL[...数据...]0000 ^ 同步块 ^ 型手柄 ^ 长度数组 ^ 空

每个部分都是一个系统DWORD。在 64 位窗口上,如下所示:

单维,内置型 SSSSSSSSTTTTTTTTLLLLLLL[...数据...]00000000 ^ 同步块 ^ 型手柄 ^ 长度数组 ^ 空

当它是一个对象数组(即字符串、类实例)时,布局看起来会略有不同。如您所见,添加了数组中对象的类型句柄。

单维,内置型 SSSSSSSSTTTTTTTTLLLLLLLtttttttt[...数据...]00000000 ^ 同步块 ^ 型手柄 ^ 长度数组 ^ 类型句柄数组元素类型 ^ 空

进一步看,我们发现一个内置类型,或者实际上,任何结构类型,都有自己特定的类型处理程序(所有uint 共享相同,但int 具有不同的数组类型处理程序然后是uintbyte)。所有对象数组共享相同的类型处理程序,但有一个额外的字段指向对象的类型处理程序。

关于结构类型的注意事项:可能并不总是应用填充,这可能会导致难以预测结构的实际大小。

仍然不是 56 字节...

要计入 AZ 答案的 56 个字节,我必须做出一些假设。我假设:

  1. 同步块和类型句柄计入对象大小;
  2. 保存数组引用(对象指针)的变量计入对象的大小;
  3. 数组的空终止符计入对象的大小。

一个同步块被放置在变量指向的地址之前,这使得它看起来它不是对象的一部分。但事实上,我相信它是并且它计入内部 2GB 限制。添加所有这些,我们得到,对于 64 位系统:

ObjectRef + 
Syncblock +
Typehandle +
Length +
Null pointer +
--------------
40  (5 * 8 bytes)

还没有56。也许有人可以在调试期间查看 Memory View 以检查数组的布局在 64 位窗口下的样子。

我的猜测是这样的(选择、混合和匹配):

  • 2GB 永远不可能,因为这是下一段的一个字节。最大的块应该是2GB - sizeof(int)。但这很愚蠢,因为 mem 索引应该从零开始,而不是一;

  • 任何大于 85016 字节的对象都将放在 LOH(大对象堆)上。这可能包括一个额外的指针,甚至是一个包含 LOH 信息的 16 字节结构。或许这也算到极限了;

  • 对齐:假设 objectref 不计数(无论如何它在另一个 mem 段中),总间隙为 32 个字节。系统很可能更喜欢 32 字节边界。重新审视内存布局。如果起始点需要位于 32 字节边界上,并且它需要为之前的同步块提供空间,则同步块将在前 32 字节块的末尾结束。像这样的:

      XXXXXXXXXXXXXXXXXXXXXXXXSSSSSSSSTTTTTTTTLLLLLLLLtttttttt[...data...]00000000
    

    其中XXX.. 代表跳过的字节。

  • 多维数组:如果您使用 Array.CreateInstance 动态创建具有 1 维或更多维的数组,则将使用两个额外的 DWORDS 创建一个单独的 dim 数组,其中包含维度的大小和下限(即使您只有一个维度,但前提是下限指定为非零)。我发现这不太可能,因为如果您的代码中出现这种情况,您可能会提到这一点。但这会使总开销达到 56 字节;)。

结论

根据我在这个小研究中收集到的所有信息,我认为Overhead + Aligning - Objectref 是最有可能和最合适的结论。然而,一位“真正的”CLR 大师或许能够为这个特殊的主题提供一些额外的信息。

这些结论都不能解释为什么 16 或 32 字节的数据类型分别有 48 和 32 字节的间隙。

感谢一个具有挑战性的主题,在我的过程中学到了一些东西。当一些人发现这个新答案与问题更相关时,也许有些人可以取消投票(我最初误解了这一点,并为这可能造成的混乱道歉)。

【讨论】:

  • 提问者在 x64 上,所以这个问题不是进程地址空间净空,这是 .NET 在这些问题被消除后的限制。提问者可能会也可能不会愿意将 GC 引入他的工作流程。
  • 嗯,问题标题是 "[..]ACTUAL maximum [..] 一个 .NET 数组 [..] 可以分配?"。对于 actual,我了解它是您在阵列中可以 使用的,而不是系统可能拥有但您不能使用的。无论架构如何,上面的代码都为您提供了实际可用的内容。我不明白为什么这不是问题的答案。
  • 小心,我相信 OutOfMemoryException 在未来的 .Net 版本中是无法捕获的。
  • 是和不是。 OOM 并不总是无法捕获的。在这种情况下,因为您保留内存,而不是使用它,所以很容易恢复。 .NET 的未来版本没有禁止捕获 OOM 的计划。问题是不同类型的:OOM 通常很难恢复。我并不是说代码在实践中是一个好主意,但它回答了问题的“如何”。
  • 致反对者:我不介意,但请亲切地解释我的代码中的错误信息是什么。
【解决方案2】:

所以,我运行了一个 li'l 程序来找出一些硬值,这就是我发现的:

  • 给定一个类型 T,f(sizeof(T)) = N + d

    • 其中 f 是实际的最大大小 Ts 的数组。
    • N 是理论值 最大尺寸,即: Int32::MaxValue / sizeof(T)
    • 而 d,是 N 和 f(x) 的差值。

结果:

  • f(1) = N - 56
  • f(2) = N - 28
  • f(4) = N - 14
  • f(8) = N - 7
  • f(16) = N -3
  • f(32) = N - 1

我可以看到,每次尺寸重复时,实际尺寸和理论尺寸之间的差异都会折叠,但不是 2 的幂。有什么想法吗?

编辑:dT 类型元素的数量。要以字节为单位查找d,请执行sizeof(T) * d

【讨论】:

  • 您的 li'l 程序听起来有些有趣(虽然 OT),但如果我们能看到它,那就更有趣了。 关于差异: Int32.MaxValue 是一个奇数。如果32 是最后一个示例中值类型的字节大小,则d(32) == Int32.MaxValue - (Int32.MaxValue / 32) * 32 产生31,即d 的二减一的幂。顺便说一句,我很难理解这是如何相关的,您在问题中询问了 actual 最大值,而您自己的答案是高度理论化的(在某种程度上是错误的,因为它依赖于实现: Mono(甚至 Micro?)还有其他最大值)。
  • @abel,什么是旧约?我不明白为什么这不相关且高度理论化。我要求实际的最大“元素数”(对不起,我引用了这个,但在你的 cmets 中你两次省略了它)。我并不是要粗鲁,也不是什么,但是,你们的一些 cmets 有点像一个“duh questioner”,这是不被欣赏的,或者至少,我觉得。
  • @AZ,如果您有这种感觉,我深表歉意。我喜欢这个问题,否则我会花这么多时间吗? OT 的意思是“离题”,当然,这是我的看法(不是本地人,我有时听起来很苛刻,抱歉)。要么我不理解你的代码,要么我不理解你的问题,我写了“OT”,因为我认为这是一个支线(尽管如此)。我认为您需要知道有多少字节可用,然后才能将其除以元素的大小,以找到总数。我在你的代码中没有看到。不过,我想知道如何解释它。我错过了什么?
  • @Abel,我很高兴你喜欢我的问题。但总的来说,我认为我的问题比你想象的要简单。假设我有足够的可用物理内存(现在 4 GB 可用),这让我认为我可以轻松地在一维数组中分配 2 GB,但是,如果我执行类似 new double[Int32::MaxValue / 8] 的操作,它将引发内存异常,但 (Int32::MaxValue / 8) - d,对于 d >= 7 不会。这比 2 GB 少 56 个字节。
  • 另一个注意事项,只是为了理智,我创建了两个数组 ((Int32::MaxValue / 8) - 7) 双打只是为了确保,当我第一次尝试时,我没有t 耗尽物理内存。鉴于我可以同时拥有这两个数组,这让我认为物理内存不是问题:)。
【解决方案3】:

您的进程空间限制为 2GB,除非您 [编译为 anycpu 或 x64] 并在 x64 进程中运行 [在 x64 机器上]。这可能是您实际遇到的情况。无论如何,计算你在这个过程中的净空不是一门精确的科学。

(Nitpickers 角落:有一个 /3GB 开关和堆栈的其他边缘情况会影响这一点。此外,该进程也需要分配虚拟或物理空间。关键是目前,大多数与任何 .NET 限制相比,人们会更频繁地遇到操作系统每个进程的限制)

【讨论】:

  • 在您在 x64 环境中进行编辑之前,这显然是一个更相关的答案(我认为您已经检查过您肯定被加载为 x64?)
  • @Ruben:据我所知,无论您是在 32 位还是 64 位中运行,CLR 的最大对象大小限制为 2GB。即使 OP 有 3GB 的可用 RAM,我也不会对他们找不到 2GB 的连续空间来分配数组感到非常惊讶。
  • @Luke:我的意思正是如此(这是在 OP 提到 x64 之前)- 在一个进程中有 2GB 空闲空间的可能性很低(我假设一个简单的测试,因此CLR 进程的大对象堆中的连续性不是问题),因为在放入操作系统开销、线程堆栈和 CLR 使用之前,最多只有 2GB 的可寻址空间。但在问题中,他说他是 x64,所以假设它是一个 x64 进程并且有可用的 [物理 + 虚拟] 内存来满足需要,我们受限于 CLR 限制。我的观点只与 x86 有关。
  • 你说的不一样。提问者不能说 var myDoubles = new double[MyMagicFunctionWhichAlwaysTellsMeHowMuchICanAllocate()]。即使计算出正确的值,也存在可用空间可能已用完的竞争条件。这在解释我的角度时是否有意义?
  • @Abel:是的,锯齿状数组更好地应对[相对]低内存,您的示例很有趣。但这不是提问者的上下文,他在 x64 上并且想准确了解 .NET 限制是假设他在 x64 上并且可用进程 VM 不是瓶颈。但我可能错了。我敢肯定@AZ 现在随时都会介入 cmets 和投票(他的回答是最好的 IMO - 这是唯一一个有人投票支持的人!)
【解决方案4】:

更新:我的 other answer contains the solution,但我将其留给有关 Mono、C#、CLR 链接和讨论线程的信息

数组的最大大小受整数大小的限制,而不是它所包含的对象的大小。但是 .NET 中的任何对象都限制为 2GB,句号(感谢 Luke 并参见 EDIT),这限制了数组的总大小,即单个元素的总和加上一些开销。

它阻塞系统的原因是系统的可用内存。而且win32进程的系统只允许你使用2GB的内存,你的程序和CLR甚至在你启动你的数组之前就已经使用了相当多的内存。您可以将其余部分用于您的数组:

int alot = 640000000;
byte[] xxx = new byte[1U << 31 - alot];

这取决于你的 CLR 是如何配置的,你是否会耗尽内存。例如,在 ASP.NET 下,您默认绑定到机器总可用内存的 60%。

编辑:This answer to a related post 更深入地探讨了主题和 64 位的问题。在 64 位系统上是可能的,但只能使用变通方法。它指向this excellent blog post on the subject,它解释了BigArray&lt;T&gt;

注意 1:其他 CLR,即 Mono,只允许大于 2GB 的对象。

注意 2:限制你的不是语言。这在 C# 中编译得很好,但是尝试和完善一台不会抛出它的机器是一个相当未来主义的想法(坦率地说,Array 类中保存长度的字段是int,这意味着这将 总是抛出 32 位,但不一定,虽然极有可能,在任何 64 位实现上):

int[] xxx = new int[0xFFFFFFFFFFFFFFFF];  // 2^64-1

【讨论】:

  • 如果它是 ref 类型,那是有道理的。 Questioner 有双精度类型,它们是值类型并内联到数组对象中
  • 即使有引用,您仍然会受到数组实际占用大小的限制 - 4*count 或 8*count,具体取决于 CLR。​​
  • @Abel:元素的大小确实限制了元素的最大数量,至少在理论上是这样。尽管当前的最大元素数限制为 int.MaxValue,但您也受到 .NET 的 2GB 最大对象大小限制的限制。所以理论上boolbyte 的数组在达到2GB 限制之前可能具有接近2^31 的元素,而long 数组的理论最大值只能约为2^28 接近 2GB 之前的元素。
  • 同意,你是。问题是“2GB 是 .NET 的限制”。这取决于系统。如果系统是 32 位的,那么可以。如果是 64 位,则没有。而且...启用UAE(术语)后,可能会有更多内存可用,但我不确定.NET 是否支持UAE。
  • 据我所知,.NET 中的最大对象大小限制为 2GB。过去肯定是这样,我不知道最近有什么变化,尽管我不是专家,也很难找到任何确定的信息。
【解决方案5】:

您还需要将指针大小 (System.IntPtr.Size) 添加到每个 sizeof(T) 以说明指向任何给定数组元素中对象的指针。

【讨论】:

  • 不知道你在做什么。如果它是一个值类型(OP的谈话双打),那么每个项目就没有这样的开销 - 尽管数组有一个 .Length 和其他小的簿记包袱,但这些都不是基于每个项目的.如果它是 ref 类型,那么您将存储 IntPtr.Size 的单位。你能澄清一下吗?
  • 抱歉,我不在 - 我想我误解了这个问题。我没有注意到分配是双精度数(值类型)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-09
  • 2022-11-27
  • 1970-01-01
  • 1970-01-01
  • 2021-12-06
相关资源
最近更新 更多