为什么 Node Javascript 中大型数组对象的内存分配行为会发生变化? (Docker 个容器)
Why does memory allocation behavior change in Node Javascript for large Array objects? (Docker container)
这个问题是关于内存分配的。我在此示例中使用数组,因为它复制了另一个与第 3 方库中的数据库代码相关的更复杂问题的行为。我需要了解为什么内存分配行为在“容器中的节点”环境中以这种方式发生变化。
我 运行 node.js (12.18.4, lts-stretch) 在 Docker 桌面社区 (2.3.0.5, Windows) 容器中。容器的内存限制为 2GB。 (但是,无论我分配给 Docker 多少内存,我都会看到相同的行为。)
在节点中,此语句按预期工作:
var a = Array(32 * 1024 * 1024).fill(0);
然而,当这条语句执行时,节点开始无限制地分配内存,就好像它陷入了一个无限循环:
var a = Array(32 * 1024 * 1024 + 1).fill(0);
当 运行 node.exe 来自 Windows PowerShell 提示时,我没有看到上述行为——仅当 运行 节点容器(https://hub.docker.com/_/node ).
当容器中有 运行 个节点时,为什么内存分配在 32MB + 1 个元素时无法正常工作?
这里是 V8 开发人员。简而言之:Mike 'Pomax' Kamermans 的猜测是正确的。
32 * 1024 * 1024 == 2**25 是 new Array(n)
分配长度为 n
的连续(“C-like”)后备存储的上限。用零填充这样的后备存储相对较快,不需要进一步分配。
对于较长的长度,V8 将以“字典模式”创建数组;即它的后备存储将是一个(最初是空的)字典。填充这个字典比较慢,首先是因为字典访问有点慢,其次是因为字典的后备存储需要增长几次,这意味着复制所有现有元素。坦率地说,令我惊讶的是数组仍处于字典模式;理论上它应该在达到一定密度时切换到 flat-array 模式。有趣的是,当我 运行 var a = new Array(32 * 1024 * 1024 + 1); for (var i = 0; i < a.length; i++) a[i] = 0;
时,就会发生这种情况。看起来 fill
的实现可以在那里得到改进;另一方面,我不确定这个案例在实践中有多重要...
旁注:
at 32MB + 1 elements
我们这里讨论的不是 32 MB。每个条目占用 32 或 64 位(取决于平台和 pointer compression),因此 2**25 个条目需要 128 或 256 MB。
node starts allocating memory without limit, as if it were stuck in an infinite loop
操作确实在一段时间后终止(在我的机器上大约 7 秒),内存分配峰值达到 900 MB 多一点。原因是,如果您实际使用所有条目,那么字典模式的 space 效率明显低于平面数组后备存储,因为每个条目都需要存储其索引、属性和值本身,加上字典它们的本质需要一些未使用的容量来避免过多的哈希冲突。
I am using Array in this example because it replicates the behavior of another, more complex problem
考虑到这里看到的行为对数组的具体程度,我确实想知道这个简化的案例反映您在其他地方看到的行为的准确程度。如果您拥有的真实代码 而不是 分配和填充巨大的数组,那么无论发生什么,都可能是其他原因。
这个问题是关于内存分配的。我在此示例中使用数组,因为它复制了另一个与第 3 方库中的数据库代码相关的更复杂问题的行为。我需要了解为什么内存分配行为在“容器中的节点”环境中以这种方式发生变化。
我 运行 node.js (12.18.4, lts-stretch) 在 Docker 桌面社区 (2.3.0.5, Windows) 容器中。容器的内存限制为 2GB。 (但是,无论我分配给 Docker 多少内存,我都会看到相同的行为。)
在节点中,此语句按预期工作:
var a = Array(32 * 1024 * 1024).fill(0);
然而,当这条语句执行时,节点开始无限制地分配内存,就好像它陷入了一个无限循环:
var a = Array(32 * 1024 * 1024 + 1).fill(0);
当 运行 node.exe 来自 Windows PowerShell 提示时,我没有看到上述行为——仅当 运行 节点容器(https://hub.docker.com/_/node ).
当容器中有 运行 个节点时,为什么内存分配在 32MB + 1 个元素时无法正常工作?
这里是 V8 开发人员。简而言之:Mike 'Pomax' Kamermans 的猜测是正确的。
32 * 1024 * 1024 == 2**25 是 new Array(n)
分配长度为 n
的连续(“C-like”)后备存储的上限。用零填充这样的后备存储相对较快,不需要进一步分配。
对于较长的长度,V8 将以“字典模式”创建数组;即它的后备存储将是一个(最初是空的)字典。填充这个字典比较慢,首先是因为字典访问有点慢,其次是因为字典的后备存储需要增长几次,这意味着复制所有现有元素。坦率地说,令我惊讶的是数组仍处于字典模式;理论上它应该在达到一定密度时切换到 flat-array 模式。有趣的是,当我 运行 var a = new Array(32 * 1024 * 1024 + 1); for (var i = 0; i < a.length; i++) a[i] = 0;
时,就会发生这种情况。看起来 fill
的实现可以在那里得到改进;另一方面,我不确定这个案例在实践中有多重要...
旁注:
at 32MB + 1 elements
我们这里讨论的不是 32 MB。每个条目占用 32 或 64 位(取决于平台和 pointer compression),因此 2**25 个条目需要 128 或 256 MB。
node starts allocating memory without limit, as if it were stuck in an infinite loop
操作确实在一段时间后终止(在我的机器上大约 7 秒),内存分配峰值达到 900 MB 多一点。原因是,如果您实际使用所有条目,那么字典模式的 space 效率明显低于平面数组后备存储,因为每个条目都需要存储其索引、属性和值本身,加上字典它们的本质需要一些未使用的容量来避免过多的哈希冲突。
I am using Array in this example because it replicates the behavior of another, more complex problem
考虑到这里看到的行为对数组的具体程度,我确实想知道这个简化的案例反映您在其他地方看到的行为的准确程度。如果您拥有的真实代码 而不是 分配和填充巨大的数组,那么无论发生什么,都可能是其他原因。