【问题标题】:ArrayList and HashSet memory allocation strange test resultsArrayList 和 HashSet 内存分配奇怪的测试结果
【发布时间】:2016-12-03 00:53:24
【问题描述】:

我受到这个话题的启发:Performance and Memory allocation comparision between List and Set 实际运行一些测试并测量ArrayListHashSet 之间的性能差异。

在提到的主题中,最让我感兴趣的答案 (link) 说:

对于相同数量的元素,HashSet 消耗的内存大约是 ArrayList 的 5.5 倍

ScalaMeter 的帮助下,我想确认一下。

我做了两个简单的测试,将10000100000 元素添加到ArrayListHashSet将初始大小设置为最大值并没有改变结果。我用两种类型测试了这些集合:

  • Int(将连续数字 0 到 100000)
  • String(使用 Apache RandomStringUtils 放置随机字符串)

代码在我的存储库here 中可用。

运行这些,给了我这个结果:

  • X 轴 - 大小 -> 集合的大小
  • Y 轴 - 值 -> 使用的 kB 量

对于持有Int的收藏:

对于持有大小为 10 的 String 的集合:

对于持有大小为 50 的 String 的集合:

问题:

引用答案中提到的理论发生了什么?是假的吗?或者我这边可能有什么错误?

谢谢:)!

@andrzej 回答后更新 我再次更新了代码(和存储库)。结果越来越好,但结果仍然没有 5.5 倍不同。我现在正在检查更多内容。

【问题讨论】:

  • 你是如何构建这些元素的?您是否保证它们彼此不同?
  • @JFMeier 请查看更新(并检查 github 上的代码 :-))
  • 您的结果看起来正确。 5.5 是与集合结构开销相关的理论速率,它没有考虑对象大小。物体越大,这个比率就越低,你的测试表明了这一点。让你的字符串长度为 50 个字符而不是 10 个字符,数组和集合之间的差异会更小。
  • @AndrewLygin 我添加了 50 个字符长字符串的图表。看来您是对的,我会尝试做一些数学计算并计算尺寸。

标签: java scala collections performance-testing scalameter


【解决方案1】:

请添加测量对象作为返回值。

measure method "Int" in {
  using(sizes) curve listS in { i =>
    val c = new util.ArrayList[Int](i)
    (0 until i).map(t => c.add(t))
    c // return c
  }

  using(sizes) curve setS in { i =>
    val c = new util.HashSet[Int]()
    (0 until i).map(t => c.add(t))
    c // return c
  }
}

【讨论】:

    【解决方案2】:

    我认为,这里有两个问题:

    1. 正如 Andrzej 所说,您不会从基准 sn-ps 中返回您的集合。 Scalameter 通过在基准执行之前和之后执行 GC 来测量占用空间(查找详细信息 here)。如果你不返回集合,它只是被后测GC从内存中删除,测试结果是没有用的。它解释了为什么您的测试中的内存占用仍然很小(每个对象大约四个字节)并且没有差异。但这并不能解释为什么随着集合大小的增加,足迹也会增加,这就是第二个问题。

    2. 一些垃圾收集器(尤其是 CMS 和 G1)不保证在执行垃圾收集后所有死对象都会从内存中删除。如果您的 JVM 选择了这些收集器之一(或者如果您手动指定它),则可以解释内存占用的上升趋势。您可以通过为您的测试提供-XX:+PrintFlagsFinal 选项并查找UseG1GCUseConcMarkSweepGC 标志的值来检查正在使用的收集器。

    【讨论】:

    • 在最后一行之前是 (0 until i).map(t => c.add(t)) - 它是大小为 i 的 Seq[Boolean]
    • 这证实了第一个问题并解释了那些“每个对象四个字节”。 Seq[Boolean] 用于计算占用空间,而不是在最终内存测量之前由 GC 删除的实际集合。只需从您的 sn-ps 中返回真实的集合,您就会看到不同之处。
    【解决方案3】:

    引用答案中提到的理论发生了什么?是假的吗?

    我们可以做一些计算来得到一个估计:

    让我们看看ArrayListHashMap 的OpenJDK 源代码(因为HashSet 只是HashMap 的包装)以获得提示。

    假设您有 n 元素要存储。

    数组列表

    元素存储在字段transient Object[] elementData; 中。所以elementData 的长度必须至少为n
    假设您使用new ArrayList<>(n) 实例化列表,因此elementData.length 恰好是n。 那么您的列表的大小是n*c 字节(其中c 是对象引用的大小)。这里我忽略了列表的size字段和object header

    哈希映射

    HashMap 将元素存储在transient Node<K,V>[] table; 中,其中节点具有字段

    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    

    然后,为了存储 n 元素,您需要 n 节点或 n*(3*c + 4) 字节,即每个节点有 3 个对象引用 - 3*c 字节 - 和一个 int - 4 字节。
    根据HashMap javadoc

    当哈希表中的条目数超过负载因子和当前容量的乘积时,对哈希表进行重新哈希(即重建内部数据结构),使哈希表的条目数大约是原来的两倍桶。

    基于此,我估计table.length == 2*n
    总结一个 hashmap 需要n*2*c + n*(3*c + 4) = n*5*c + n*4 个字节。

    总结

    现在假设您有一个 64 位 JVM,并且对象引用的大小是 8 个字节(即 c = 8)(让我们忽略像 compressed oops 这样的东西)。 然后n*5*c + n*4 = n*5*8 + n*4 = n*44n*c = n*8
    最后n*44 / n*8 = 5.5

    因此,HashSet 消耗的内存大约是 ArrayList 的 5.5 倍的原始理论似乎很合理,而且您的测量结果似乎有问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-19
      • 2011-03-13
      • 2014-04-13
      • 1970-01-01
      • 2017-03-24
      • 2012-07-01
      • 1970-01-01
      • 2013-02-10
      相关资源
      最近更新 更多