【问题标题】:Memory efficient way to store list of integers存储整数列表的内存有效方法
【发布时间】:2014-01-29 19:28:53
【问题描述】:

我正在做一些 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 中。也许将这些数组存储在树结构中并制作我的对象指出它会有帮助吗?

您有什么建议吗?

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

我正在考虑使用按位运算将这些数组转换为“大整数”,即创建一个具有某个类的大整数,设置位 1、5、74、1013,将大 int 转换为字符串(数组8 个字节)并存储字符串,但并不总是有好处(例如,数组 [4000] 将表示为 500 字节长的字符串...)

项目范围(没用,但有人问过我)

这应该是没用的,但我被问过好几次了,我把它放在这里。

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

那些细胞就是我在描述中所说的对象 A。每个单元格包含一个 id 列表(正整数),它指向单元格相交的 Sphere 对象。

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

【问题讨论】:

  • 也许你可以用 dups 原样存储数组,然后记住所有可能使用数组计算的函数。 JS 中的大数组通常不是问题,您会发现与它们交互的方法会导致性能差异。
  • 我的建议是发布您的代码,以便我们能够真正看到问题所在。不知道你的问题是基于上述...
  • 我实际上无法发布数千行代码...我什至不知道从哪里开始将这个问题减少到 SO 上的几行...
  • 您确定您提到的 2GB 仅来自您的整数列表吗?因为这意味着你有超过十亿个数字(你说它们是空头,如果它们是常规整数,你仍然需要十亿个数字)。那是很多数字。也许您将纹理存储在某个地方,这会占用大量内存....
  • 不管怎样,看看this SO question。这是关于 OpenGL 中的顶点数组,但基本原理与您尝试做的相同。那里提到的优化也可以应用于您的情况。

标签: javascript arrays optimization integer


【解决方案1】:

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

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

【讨论】:

  • 这看起来不错,但我怎样才能为整数列表找到一个有效的散列函数而不会发生冲突?我应该将此列表转换为按位运算的字符串吗?
  • 我认为javascript会自动调用toString方法。每个集合的字符串都是唯一的,因此它应该作为键。字符串内部有自己优化的哈希函数,因此在最小化冲突方面应该是相当合理的。
【解决方案2】:

对于这种大小的数组来说,这确实看起来有很多内存,如果没有看到你的源代码,我会仔细看看你在哪里对数组执行操作。

  • 某些数组操作会创建数组的副本。
  • 在数组中的项目集合增加时将数组初始化为较小并多次扩展它会产生很大的内存开销。

【讨论】:

  • 嗯,根据要处理的数据,我有 100k 到 800k 对象 A 包含这些数组,因此尽管 RAM 中有 2GB,但程序的内存消耗似乎是正常的。这并不意味着我不能优化它,但我看起来好像没有哪里有错误。
【解决方案3】:

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

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

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

【讨论】:

  • 这是解决“减少 javascript 代码占用”问题的一个非常通用的解决方案。不幸的是,我真的买不起那些(除了第一个,我不太明白)。我确定了“大内存占用”的原因,我想找到一种不同的数据结构来尽可能有效地解决这个问题
【解决方案4】:

这是我的尝试:

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

  2. 您也可以尝试将值包装在一个不可变的对象(例如 OpNumeric)中。每次您需要一个数值时,您都向 NumericManager 请求一个包装的实例并存储对 OpNumeric 的引用。这样,所有 5 的值都将存储对同一对象 OpNumeric(5) 的引用;我不确定 JS 中引用的大小是多少(它可能取决于实现和机器功能),但值得一试。 OpNumeric 也是实现 (1) 的理想选择。这会稍微降低性能,并且可能会暂时增加内存使用量,但这应该只在解析 OpNumeric 引用时才会发生。

  3. 如果您动态生成数组(而不是通过逐个附加值),也许值得散列并重用它们。因为你已经知道范围,你可以想出一个尽可能少的冲突的哈希值;即使您有几次冲突,通过逐个值比较来选择正确的引用应该是相当 CPU 友好的。

抱歉,这不是一个直接的答案,但是对于这些事情,没有直接的方法可以实现目标。也许值得关注asm.js。似乎和你正在做的事情有一点共同点。

干杯,

【讨论】:

  • 1/ 这显然是我的想法之一。 2/ 我认为用指针引用替换整数值是不值得的。 Ref 通常在内存中大小相同或更大。 3/ 这又是对的,可能值得散列和重用这些数组。但是如果我有一个非常完美的双射散列函数,最好不要散列任何东西并使用它来压缩数据。
  • 按位运算最大的阻塞问题是,如果你有大数字的小数组,比如 [4000],那么你将有 1 个设置了第 4000 位的整数......所以 500 字节storage 只存储这个数组...
  • 你能再描述一下你的项目在做什么吗?有什么选择和目标。在不改变太多逻辑的情况下减少内存占用是您的唯一目标吗?例如。你能看看你的模型的部分处理(这样你就不需要一次加载所有数据)吗?
  • 我不太愿意提供有关该项目的更多信息,因为我认为我们将失去我被允许做/不做的事情的重点。我在这里给你一些提示:我有一个名为octree 的内存结构,其中节点是对象A,包含这些列表。我有大量节点,对此我无能为力。这些单元格包含一个整数列表(对象 ID),它们基本上代表了一些要被淹没的球体。每个列表代表这个八叉树节点的球体相交。
  • 由于许多节点与相同的球体相交,因此它们在内存中具有相同的(重复的)列表。我现在正在寻找一种方法来减少程序的内存占用,而不会过多地影响对这些列表的访问时间。不同的节点可以共享同一个列表,因为它们不会修改它。
【解决方案5】:

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

【讨论】:

  • 这确实可以减小数组大小。好的。将尝试使用 Uint16Array。
  • 有很多答案。这并不是我预期的真正重大改进,但它可能是最好的。
【解决方案6】:

我敢于说出一个神话。

在这个神话思想中,

  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,用于存储唯一的重复数组。

在这个神话之上,这就是我希望我们能做到的......

  1. 当你创建一个新对象(Your A)时,你会sort它的数组,slice从静态数组中从这个A的第一个值,到这个A的最后一个值,找到一个diff sliced 中的一个,以及您当前 A 的数组,存储哪个更小。
  2. 考虑到这种情况,示例条目(metadata 用于 A 的对象)将如下所示:"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)。您的代码将读取这些数据以在运行时动态构建您的网格。

还请注意,有很多事情需要解决,比如切片和存储小数组完全不明确......但我相信metadata(存储关于数据而不是数据的数据)在这些类型中的情况绝对值得花。

【讨论】:

    【解决方案7】:

    我曾开发过在网页中显示分页表格的系统,其中包含多达 25000 行客户数据,包括 10 列姓名、年龄、地址、电话等,正如您想象的那样,我们一直在寻找优化该数据显示的方法。

    显然,在网页中构建一个可搜索、可排序的大表并尝试维护所有这些数据会占用大量内存(您可以想象它对 IE7/8 的影响)。

    我想出的四个解决方案:

    1. 把我从服务器得到的JSON数据,拆分成可管理的 索引JSON字符串并将其放入本地存储,然后根据需要访问数据。

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

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

    4. 如果缺少这些选项中的任何一个,用户就不得不受点苦。 他们需要输入一些搜索参数,然后他们就会看到 仅通过 ajax 调用返回的数据且仅可管理 块。但如果他们没有本地存储、Flash 或 Java 可用,然后他们得到了他们应得的。 ;)

    【讨论】:

      【解决方案8】:

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

      function IntegerArray(integers) {
        this.ranges = [];
        // Convert integers to ranges (Don't have time to overview algorithm,
        // but I think a good start would be sorting the integers and searching
        // for gaps)
      }
      IntegerArray.prototype = {
        insert: function (n) {
          // Could achieve O(log n) time complexity with binary search since
          // the ranges are sorted.
          // * n is between two ranges = insert [n, n] range.
          // * n is 1 less than the start of a range = decrease range start by 1.
          // * n is 1 more than the end of a range = increase range end by 1.
        },
        remove: function (n) {
          // Also O(log n) time complexity with binary search.
          // * n has [n, n] range = remove range.
          // * n is at beginning of range = shift range.
          // * n is at end of range = pop range.
          // * n is in middle of range = split range.
        }
      };
      

      另一方面,如果您的数组有许多孤立的整数,这可能不是您想要的,因为它们每个都需要两个整数而不是一个。


      在考虑了更多之后,我想到您还可以将数组存储为以下内容:

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

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

      【讨论】:

        【解决方案9】:

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

        我认为使用哈希表的想法更有价值。但是这里有一个权衡,因为它取决于你的球体是如何分布的。你在你的问题中说:

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

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

        Imo,考虑直接列出对象,而不是使用和保留它们的 ID。无论如何,每个对象都会存在,并且我认为出于性能原因,每个对象都需要保留相交球体数组。所以最好不要增加额外 ID 的开销(内存 + 访问时间)。

        更笼统地说(如果不完全了解您在做什么,很难更准确):考虑重新访问您的数据模型,以便从一开始就提高内存效率;不要保留不需要的对象,并尝试使用对象图而不是 ID 引用的对象哈希。

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

        【讨论】:

          【解决方案10】:

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

          一种方式

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

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

          [ 10, 13, 1029 ]
          

          可以转换为大小为 32 的块,其中 10 和 13 将落在同一个块中, 并且可以由 (1

          如果您的大多数数字彼此非常接近 - 彼此在 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 个额外的 shortint。这仅在数组“聚集”时才有效。如果大多数数字分散在 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, 74, 1013, 1089 ],当你到达键 1013 时你发现没有 1089 键,所以你知道这不是重复的,将它存储在主数组中,恢复它的索引 - 比如 1234 - 并将 "1089": { "-1": 1234 } 添加到 1013 键。

          添加一个数组相当快,访问它的值也是如此。

          内存方面,值得吗?每个数组而不是 N 个整数现在由 N 个整数加上 (N+1) 个字典组成,每个字典至少有一个整数,所以我想说成本在三倍和四倍之间。如果重复数组非常多,这可能是有利的;否则可能不是。如果少于三分之一的数组是重复的,则很可能不会。

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

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

          扭曲

          代替上面的数组和树,您可以散列列表并将散列用作 id。但是,除非您使用字符串化列表(由您或由 JS 进行字符串化 - 第一种方法更节俭,第二种方法更快)作为键,否则这可能会发生冲突。在这种情况下,您平均需要,例如,密钥中每个整数需要四个字节;不过,较小的数字会更轻,因为“1.12.13.15.29”只有大约 14 个字节左右。唯一数组的内存占用大约是三倍,而重复数组的内存占用为零;再次强调,与非重复项相比,有多少重复项。

          【讨论】:

            【解决方案11】:

            对 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 的位集,那么它只有 10 位。如果第 1 位为真,则列表中有 1,如果第 10 位为真,则列表中有 10。所以你切换的阈值是 2048 个元素。 2048 个整数是 65536 位,但使用 bitset 您可以编码 65536 个元素,而不仅仅是 2048。

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

            【讨论】:

              【解决方案12】:
              1. 找到一种更节省内存的方式来存储这些列表

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

              (也许是大数字的位数组?)

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

              想办法避免重复。

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

              你有什么建议吗?

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

              【讨论】:

              【解决方案13】:

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

              由于 JavaScript 对特殊 Unicode 字符(例如组合重音字符甚至无效字符)是愚蠢的,length 将按您的预期工作。也就是说,它会给你“charCodes”的数量而不是 Unicode 字符的数量。

              【讨论】:

                【解决方案14】:

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

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

                【讨论】:

                  猜你喜欢
                  • 2017-01-28
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-07-19
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多