对象的大数组JSON.parse()使用更多的内存比它应该的方式
我生成一个〜200'000元素的对象数组(使用map
内部的对象文字符号,而不是new Constructor()
),并保存一个JSON.stringify
的d版本到磁盘,它占用了31 MB包括换行符和每个缩进级别( JSON.stringify(arr, null, 1)
)。
然后,在一个新的节点进程中,我将整个文件读入一个UTF-8string并传递给JSON.parse
:
var fs = require('fs'); var arr1 = JSON.parse(fs.readFileSync('JMdict-all.json', {encoding : 'utf8'}));
根据小牛的活动监视器,节点内存使用量约为1.05GB! 即使在terminal上键入,我感觉在我的古老的4 GB RAM机器上还是比较懒的。
但是,如果在一个新的节点进程中,我将文件的内容加载到一个string中,在元素边界处JSON.parse
, JSON.parse
每个元素,表面上获得相同的对象数组:
var fs = require('fs'); var arr2 = fs.readFileSync('JMdict-all.json', {encoding : 'utf8'}).trim().slice(1,-3).split('\n },').map(function(s) {return JSON.parse(s+'}');});
节点只使用〜200 MB的内存,并没有明显的系统滞后。 这种模式在节点的许多重新启动中持续存在: JSON.parse
整个数组在parsing元素的过程中占用了一大笔内存,而且更高效地处理内存。
为什么在内存使用上有如此巨大的差距? 这是与JSON.parse
防止高效隐藏类生成在V8中的问题? 如何在没有切分string的情况下获得良好的内存性能? 我必须使用stream式JSONparsing吗?
为了便于实验,我已经将JSON文件置于Gist中 ,请随时克隆它。
有几点需要注意:
- 您已经发现,无论出于何种原因,对数组的每个元素(而不是一个大的
JSON.parse()
进行单独的JSON.parse()
调用会更有效率。 - 你正在生成的数据格式是在你的控制之下。 除非我误解,否则整个数据文件不一定是有效的JSON,只要你可以parsing它。
- 这听起来像是第二个更有效的方法唯一的问题是拆分原始生成的JSON的脆弱性。
这提示了一个简单的解决scheme:不是生成一个巨大的JSON数组,而是为数组的每个元素生成单独的JSONstring – 在JSONstring中没有换行符,即只使用没有space
参数的JSON.stringify(item)
。 然后将这些JSONstring与换行符(或者您知道的任何字符将永远不会出现在您的数据中)并写入该数据文件。
当您读取这些数据时,将新来的数据拆分,然后对每一行分别执行JSON.parse()
。 换句话说,这一步就像你的第二个解决scheme,但是用一个简单的string分割,而不是不得不摆弄字符数和花括号。
你的代码可能看起来像这样(真的只是你发布的简化版本):
var fs = require('fs'); var arr2 = fs.readFileSync( 'JMdict-all.json', { encoding: 'utf8' } ).trim().split('\n').map( function( line ) { return JSON.parse( line ); });
正如您在编辑中指出的那样,您可以将此代码简化为:
var fs = require('fs'); var arr2 = fs.readFileSync( 'JMdict-all.json', { encoding: 'utf8' } ).trim().split('\n').map( JSON.parse );
但我会小心这个。 它在这种情况下确实起作用,但在更一般的情况下存在潜在的危险。
JSON.parse
函数有两个参数 :JSON文本和一个可选的“reviver”函数。
[].map()
函数将三个parameter passing给它调用的函数:项目值,数组索引和整个数组。
所以如果你直接传递JSON.parse
,它将被作为第一个参数调用JSON文本(如预期的那样),但是它也传递了一个“reviver”函数的编号。 JSON.parse()
忽略了第二个参数,因为它不是函数引用,所以你可以在这里。 但是,你可以想象其他情况下你可能会遇到麻烦 – 所以当你传递一个你没有写入[].map()
的任意函数时,三重检查是一个好主意。
我想这个问题的答案暗示了一个评论,但是我会稍微扩展一下。 所使用的1GB内存大概包含大量实际上“死亡”的数据分配(因为它已经变得无法访问,因此不再被程序真正使用),但还没有被垃圾收集器。
当使用的编程语言/技术是典型的现代技术(例如Java / JVM,c#/。NET,JavaScript)时,几乎所有处理大型数据集的algorithm都可能以这种方式产生大量的碎片。 最终GC删除它。
有趣的是,可以使用这些技术来显着减less某些algorithm产生的短暂内存分配量(通过指向string中间的指针),但是我认为这些技术在JavaScript中难以或不可能使用。