内存有效的方式来存储整数列表

时间:2014-01-10 17:40:18

标签: javascript arrays optimization integer

我正在做一些javascript 3D处理,我有很多对象(比如对象A),每个对象包含一些东西和一组正整数,如[0,1,4],[它们不需要具有私有值,所有对象可以共享相同的列表.1,5,74,1013]等。 数字可以从0到数千,比如65k(短)。

剖析显示阵列正在消耗大量内存。在计算时,我的程序达到了超过2GB的分配内存,这不是一些愚蠢的预优化。

我有2条用于内存优化的线索:

  1. 找到一种更有效的存储方式来存储列表(也许是大数字位数?)
  2. 找到避免重复的方法。例如,我碰巧发现一些数组(如[0,1,2,3,4,5,6])存在于超过40 000个对象A中。也许存储数组在树结构中并制作我的对象指出它会有帮助吗?
  3. 您有建议吗?

    编辑:我忘了提它,但重要的是:列表中的每个整数都是UNIQUE。 EDIT2:唯一需要检索的是整数的SET,顺序并不重要。

    我正在考虑使用按位运算将数组转换为“Big Integers”,即使用某个类创建Big Integer,设置位1,5,74,1013,将big int转换为字符串(数组) 8字节)并存储字符串,但它并不总是增益(例如,数组[4000]将表示为500字节长的字符串...)

    项目范围(没用,但我被要求了)

    这应该是无用的回答这个问题,但我已经多次被要求了,我把它放在这里。

    我正在构建一个体积庞大的3D网格,为了简化,让我们假设我有很多球体。我知道他们的位置(中心,光线),我想在一个3D网格中绘制它们。为此,我有一个名为Octree的内存结构,允许我在我的对象表面周围的较低单元格(八叉树节点)中划分3D空间。然后我可以从细胞中构建一个网格。

    Those单元格在我的描述中称为对象A.每个单元格包含一个id列表(正整数),指向单元格相交的Sphere对象。

    事实是,分析显示,单元格数组在内存中保留了数百MB。我想减少这个数字,找到一种方法来删除所有重复项和/或如果可能的话,找到一种更有效的方法来存储肯定的id列表(可以从0到65k)。

14 个答案:

答案 0 :(得分:3)

对于那个大小的数组而言,这看起来确实有很多内存,如果没有看到你的源代码,我会仔细查看你在数组上执行操作的位置。

  • 某些数组操作将创建数组的副本。
  • 当数组中的项目集合增加时,初始化数组并将其扩展多次将产生大量内存开销。

答案 1 :(得分:2)

查看此页面https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays?redirectlocale=en-US&redirectslug=JavaScript%2FTyped_arrays,我包含低级javascript容器,我认为其中一个符合您的需求

答案 2 :(得分:1)

如果存在大量重复项,您可以尝试使用散列集(密钥和值相同的散列集)来存储整数列表。这样,如果集合中已存在密钥,则无需向其添加更多密钥。然后,您的原始对象列表将仅包含对哈希集成员的引用,而不是成员本身。

这会增加一点性能开销,但如果内存是瓶颈,那么它可能不是问题。

答案 3 :(得分:0)

在没有看到实际要求的情况下,我的第一印象是,在处理实现细节之前,您应该考虑优化所用算法的方法,以减少所涉及的数据结构的内存占用。

然后,我建议评估以下选项:

  • 尝试将普通对象用作哈希而不是数组
  • 使用Dart或Java重写您的代码,并使用GWT compiler将其编译为JS并对结果进行基准测试
  • 尝试使用WebGL shaders
  • 使用Native Client SDK或NPAPI实现代码的一部分(但请注意,不推荐使用NPAPI)

答案 4 :(得分:0)

这是我的理由:

  1. 如果你控制你的数据集,你可以尝试使用JS数字(8个字节长)来存储更多的值(可能是4个值,每个2个字节长) - 确切的内存增益取决于在实际范围内;按位操作往往非常快,因此不应该有性能损失

  2. 您也可以尝试将值包装在不可变的Object(例如OpNumeric)中。每次需要数值时,都会向NumericManager询问包装实例并将引用存储到OpNumeric。这样,所有5的值都将存储对同一对象OpNumeric(5)的引用;我不确定JS中引用的大小(可能取决于实现和机器功能),但值得尝试。 OpNumeric也是实施(1)的良好候选者。这会稍微降低性能并可能会暂时增加内存使用量,但这只应该在解析OpNumeric引用时才会出现。

  3. 如果你动态生成阵列(而不是逐个附加值),也许值得散列并重用它们。因为你已经知道了范围,所以你可以想出一个哈希,它可以让你尽可能少地碰撞;即使你有一些碰撞,通过价值比较来选择正确的参考值也应该是相当CPU的。

  4. 对不起,这不是一个直接的答案,但有了这些事情,没有直接的方法来实现目标。也许值得关注asm.js。似乎与你正在做的事情有一点共同之处。

    干杯,

答案 5 :(得分:0)

我敢于讲述一个神话思想。

在这个神话中,

  1. 您将拥有一个0-65k整数的静态数组 - 已排序。
  2. 每个A对象包含metadata的数组,以及......
    1. slice(x,y)将从步骤1的静态数组开始,从x开始,到y结束。
    2. dupeof(oldarray)实际上会在运行时返回oldarray
    3. diff计算两个数组的差异
    4. sort将对一组整数进行排序... [].sort(function(a,b){ return a-b; });
  3. 用于存储唯一重复数组的数组dupes
  4. 除了这个神话之外,这就是我希望我们能做到的......

    1. 当您创建一个新对象(您的A)时,您将sort其数组slice从静态数组中,从此A的第一个值到此A的最后一个值,找到diff d slice的{​​{1}},以及您当前A的数组,存储更小。
    2. 考虑到这种情况,A对象的示例条目(metadata)将如下所示:"0-11,[3,5]"。在简单的英语中,它应该被理解为construct an array from static array, slice from 0 to 11, and remove 3, 5 from that slice。我们会得到[0,1,2,4,6,7,8,9,10,11,12]

    3. 你必须让A的构造函数在构造时通过这个逻辑。因此,在任何时候,您只有metadata个对象(您的A:s)。并且您的代码将读取此数据以在运行时动态构建网格。

    4. 请注意,有很多事情需要解决,比如切片和存储小数组总是模棱两可......但我相信这个metadata(存储数据而不是数据的数据)这种情况绝对值得花钱。

答案 6 :(得分:0)

我曾经在系统上工作,在网页中显示包含多达二万五千行客户数据的分页表,包括10列名称,年龄,地址,电话等,您可以想象,我们一直在寻找优化数据显示的方法。

显然,在网页中构建一个可搜索的,可排序的大表并尝试维护所有这些数据是一个内存耗尽(你可以想象它对IE7 / 8做了什么)。

我想出的四个解决方案:

  1. 获取从服务器获取的JSON数据,将其拆分为可管理的 索引JSON字符串并将其放在本地存储中,然后根据需要访问数据。

  2. 如果本地存储不可用,我会将Flash对象加载到 页面,它从服务器检索数据,我使用了 ExternalInterface根据需要访问数据。

  3. 如果Flash不可用,我会将其存储在Java Applet中并进行访问 它以类似的方式。

  4. 缺乏任何这些选项,那么用户不得不忍受一点。 他们需要输入一些搜索参数,他们可以看到 只有通过ajax调用返回的数据才有可管理的数据 块。但如果他们没有本地存储,Flash或Java 可用,然后他们得到了他们应得的。 ;)

答案 7 :(得分:0)

根据数组的稀疏程度,您可能希望存储整数范围,您通常在数组中有连续的整数序列(即[[1, 5], [10, 14]]而不是[1, 2, 3, 4, 5, 10, 11, 12, 13, 14]甚至{{ 1}}因为你的代码可以假设它是成对格式化的)。如果您的数组非常稀疏,您仍然可以通过存储序列中间隙的范围来使用此方法。给你一个想法:

[1, 5, 10, 14]

另一方面,如果你的数组有许多孤立的整数,那么这可能不是你想要的,因为那些将需要两个整数而不是一个整数。


在考虑了这一点后,我发现你还可以将你的数组存储如下:

  • 起点:数组中的最小整数。
  • 一个整数数组,表示整数范围(正整数)和间隙(负整数)的大小,而不是索引本身。

这改善了数据结构的大小,但可能会降低插入/删除的性能。

答案 8 :(得分:0)

我认为,每次使用特殊结构(带位掩码的字符串或大型int)来重新创建数组的想法是不正确的。这将使您一遍又一遍地重新创建数组,并且这样做的开销(额外的CPU时间+ GC时间)可能不值得。就像在数据库中一样,计算字段对于少量数据来说是公平的游戏,但是当您有大量数据时,会在您的脸上爆炸 - 如果性能很重要,可以更好地存储计算出来的直接可用结果。

我认为使用哈希表的想法有点多了。但这里有一个权衡,因为它取决于你的球体是如何躺在周围的。你在问题中说:

  

每个单元格包含一个id列表(正整数),指向单元格相交的Sphere对象。

这,我读到:根据球体分布,你最终可能会遇到病态的情况,计算和保留一个额外的整数不仅使用额外的CPU,而且 - 最重要的是 - 最终保留的对象多于你已经有了。出于这个原因,我想我也会把它排除在外:在大多数情况下都会受益,直到它在你面前爆炸。

Imo,请考虑直接列出对象,而不是使用和保留其ID。无论如何,每个物体都将存在,并且我认为每个物体都需要保持交叉球体的阵列,无论如何都是出于性能原因。所以不妨添加额外ID的开销(内存+访问时间)。

更一般地说(没有完全理解你要做的事情,很难更精确):考虑重新访问你的数据模型,这样它开始时的内存效率会更高;不要保留您不需要的对象,并尝试使用对象图而不是ID引用的对象的哈希值。

最后,请考虑您是否确实需要所有这些对象始终驻留在内存中。因为如果没有,你也可以将这些东西放在一个持久的商店中,只保留你当前使用的实际需要的东西。

答案 9 :(得分:0)

  

正整数数组,例如[0,1,4],[1,5,74,1013]等。它们不需要具有私有值,所有对象可以共享相同的列表。数字可以从0到数千,比如65k(短)。

单程

如果数组是大对象,则将带有标志数组的int数组替换为int数组是有意义的。但由于它们是短整数,因此优势很小。

一种可能性是用数字的32位数组替换数字,例如

[ 10, 13, 1029 ]

可以转换为大小为32的块,其中10和13将落在同一块中, 并且可以由(1 <&lt; 10)|编码(1 <<;&; 13)。但要这样做,您需要每个列表包含所有计数器,这意味着2048个32位整数,其中大部分为零。

一种变体(某种运行长度编码),如果你的大多数数字彼此非常接近 - 彼此的32个整数之内)可以将每个“运行”的数字存储为一对,一个描述第一个数字N,另一个编码后面的数字,直到N + 32。所以:

 15, 21, 27, 29, 32, 40, 44

变为:

 15, (21-16=)5, (27-16=)11, 13, 16, 24, 28

然后:

 15, 1<<5 + 1<<11 + 1<<13 + 1<<16 + 1<<24 + 1<<28

这样您就可以在一个32位值中存储6个额外的shortints。这仅在阵列“聚集”时才有效。如果你的大多数数字分散,远远超过32,那绝对不值得努力。

此外,您必须考虑处理此类数字的成本,即,为了对其执行某些操作而重新填充压缩数组的成本是多少。

另一种方式

您可以尝试使用到目前为止使用的所有数组保持结构,并使用一种方法来检索给定其ID的数组。树结构将允许存储公共前缀数组,但是当您通过ID恢复它们时会遇到麻烦。所以我担心你能做的最好就是使用很长的数组:

[ [ 0, 1, 4 ],             // ID#0
  [ 1, 5, 74, 1013 ],      // ID#1
]

这意味着找出数组是否存在将会付出代价。您还需要添加索引:

{ "1": { "5": { "74": { "1013": { "-1": 1 // The array ending here has id 1

现在您存储[1,5,71,1013,1089],当您到达密钥1013时,您找不到1089密钥,因此您知道这不是重复,将其存储在主阵列中,恢复其index - 比如1234 - 并将“1089”:{“ - 1”:1234}添加到1013键。

添加数组合理快,就像访问其值一样。

记忆明智,值得吗?现在每个数组而不是N个整数由N个整数加上(N + 1)个字典组成,每个字典至少有一个整数,所以我要说成本介于三倍和四倍之间。如果重复阵列非常多,这可能是有利的;否则可能不是。如果小于,例如,三分之一的数组是重复的,那很可能不会。

此外,您现在无法轻松修改数组,因为您需要重新索引树;并且删除数组意味着重新索引两次,一次删除数组,另一次找到最后一个数组的索引;将其索引更新为刚刚腾出的索引,并移动最后一个数组以填补空白,然后将数组列表减少一个。

 [ 1 ]       [ 1 ]      [ 1 ]
 [ 2 ]       [ 2 ]      [ 2 ]
 [ 3 ]  -->        -->  [ 5 ]
 [ 4 ]       [ 4 ]      [ 4 ]
 [ 5 ]       [ 5 ]      

您可以散列列表并使用散列作为id,而不是上面的数组和树。但是,这会冒碰撞的风险,除非您使用字符串化列表(由您或由JS - 第一个方法thriftier,第二个更快的方法)作为键使用。在这种情况下,你需要平均,比如说,密钥中每个整数四个字节;较小的数字会减轻,但“1.12.13.15.29”只有大约14个字节左右。对于唯一的阵列,内存占用量将增加三倍,并且对于重复数据而言归零;再次说明,与非重复项相比,有多少重复项。

答案 10 :(得分:0)

为lucene / solr做了类似的事情。随意查看https://github.com/apache/lucene-solr/blob/trunk/solr/core/src/java/org/apache/solr/search/DocSetCollector.java 这是一个java,但我打赌你可以对javascript使用相同的想法。

简而言之,最初它们存储一个int数组

// in case there aren't that many hits, we may not want a very sparse
// bit array.  Optimistically collect the first few docs in an array
// in case there are only a few.
final int[] scratch;

但是后来如果匹配的文档数量过多,他们会切换到只有

的BitSet
bits = new OpenBitSet(maxDoc);

此处maxDoc是列表中的最大项目数。我不知道你是否能在你的任务中找到这个数字,但也许你知道列表中的N个整数永远不会更多。 (看起来你提到65k)。

所以,如果你有数字,比如1,2,3,4,5,6,7,8,9,10那么,当你有整数时,你有10 * 32位,即320位。但是如果你分配大小为10的bitset,那么它只是10位。如果第1位为真,那么列表中有1,如果第10位为真,则列表中有10。因此,您的切换阈值是2048个元素。 2048个整数是65536位,但是使用bitset可以编码65536个元素,而不仅仅是2048个。

注意:项目在这样的位集中是唯一的,顺序显然是升序。

答案 11 :(得分:0)

  
      
  1. 找到一种更有效的内存存储方式列表
  2.   

请注意JavaScript does not have integers,只有浮点数。

  

(也许是大数字位数?)

我很确定JavaScript没有任何按位运算符。 (浮点上的按位运算没有意义)

  

找到避免重复的方法。

如果可以1)检测到重复,2)将它们指向同一个底层对象,那将避免大量内存。如果您提前构建结构,这应该是微不足道的。但是在运行时检测重复会降低性能。你需要进行基准测试才能看到多少。在我的脑海中,我想看一下将数组存储在Trie中。您的对象将有一个指向数组的直接指针,但是在添加新数组时,您将通过Trie。这可以防止重复。

  

你有什么建议吗?

如果您在浏览器中运行,请查看名为asm.js的项目。这将让你实际使用整数。

答案 12 :(得分:0)

将每个数组存储为字符串。字符串本质上是一个short数组(UTF16),而interning运行时将避免为相同的数组/字符串提供额外的存储空间。使用String.fromCharCode()将UTF16数值转换为单字符字符串。使用String.charCodeAt()从字符串中提取数字。

由于JavaScript对特殊Unicode字符等内容很愚蠢,例如组合重音字符甚至无效字符,length将按预期工作。也就是说,它将为您提供“charCodes”的数量,而不是Unicode字符的数量。

答案 13 :(得分:0)

您可以使用bitset。有高效的库在JavaScript中实现位集。例如见:

https://github.com/lemire/FastBitSet.js