【问题标题】:ImmutableCollections SetN implementation detailImmutableCollections SetN 实现细节
【发布时间】:2017-07-27 13:55:29
【问题描述】:

我很难理解 java-9 ImmutableCollections.SetN 的实现细节;具体为什么需要将内部数组增加两次。

假设你这样做:

Set.of(1,2,3,4) // 4 elements, but internal array is 8

更确切地说,我完全理解为什么在 HashMap 的情况下这样做(双重扩展) - 你永远(几乎)不希望 load_factor 成为一个。例如,!=1 的值会缩短搜索时间,因为条目可以更好地分散到存储桶中。

但如果是 不可变集 - 我真的说不出来。特别是因为选择了内部数组的索引方式。

让我提供一些细节。首先是如何搜索索引:

 int idx = Math.floorMod(pe.hashCode() ^ SALT, elements.length);

pe 是我们放入集合中的实际值。 SALT 仅在启动时生成 32 位,每个 JVM 一次(如果您愿意,这是实际的随机化)。 elements.length 对于我们的示例是 8(4 个元素,但这里是 8 个 - 大小翻倍)。

这个表达式就像一个负安全模运算。请注意,在选择桶时,在HashMap中在HashMap中完成相同的逻辑事物。

所以如果我们的例子是elements.length is 8,那么这个表达式将返回任何小于8的正值(0, 1, 2, 3, 4, 5, 6, 7)

现在剩下的方法:

 while (true) {
        E ee = elements[idx];
        if (ee == null) {
            return -idx - 1;
        } else if (pe.equals(ee)) {
            return idx;
        } else if (++idx == elements.length) {
            idx = 0;
        }
    }

让我们分解一下:

if (ee == null) {
    return -idx - 1;

这很好,这意味着数组中的当前槽是空的——我们可以把我们的值放在那里。

} else if (pe.equals(ee)) {
    return idx;

这很糟糕 - 插槽已被占用,并且已经到位的条目等于我们想要放置的条目。 Sets 不能有重复的元素 - 所以稍后会抛出异常。

 else if (++idx == elements.length) {
      idx = 0;
 }

这意味着这个槽被占用了(哈希冲突),但是元素不相等。在HashMap 中,此条目将被放入与LinkedNodeTreeNode 相同的存储桶中——但此处并非如此。

所以index 递增并尝试下一个位置(有一点需要注意的是,当它到达最后一个位置时它会以圆形方式移动)。

问题是:如果在搜索索引时没有做任何花哨的事情(除非我遗漏了什么),那么为什么需要一个两倍大的数组呢?或者为什么函数不是这样写的:

int idx = Math.floorMod(pe.hashCode() ^ SALT, input.length);

// notice the diff elements.length (8) and not input.length (4)

【问题讨论】:

  • @VinceEmigh 结束其他讨论:事情往往不是非黑即白。我已经看到很多 6digits 人的答案,它们确实提供了更少的内容或更多的“模糊性”,但在几个小时内获得了大量选票。而且我已经看到好内容长期存在......并且没有任何事情发生。你看 - 零个其他答案,甚至没有这个问题的 cmets。因此,可能的解释列表可能不能解释一个很好的答案——但它可以提供思考的食物。除此之外:这不就是投票的目的吗?我将反对票作为直接反馈......好吧:我尝试这样做
  • @VinceEmigh 下次想出更好的东西。

标签: java collections java-9


【解决方案1】:

SetN 的当前实现是一个相当简单的封闭散列方案,与HashMap 使用的单独链接方法相反。 (“封闭散列”也容易混淆地称为“open addressing”。)在封闭散列方案中,元素存储在表本身中,而不是存储在从每个表槽链接的元素列表或树中,这是单独的链接。

这意味着如果两个不同的元素散列到同一个表槽,则需要通过为其中一个元素找到另一个槽来解决这种冲突。当前的SetN 实现使用线性探测解决了这个问题,其中依次检查表槽(在末尾环绕)直到找到一个开放槽。

如果您想存储 N 个元素,它们肯定适合 N 大小的表格。您总是可以找到集合中的任何元素,尽管您可能必须探测几个(或许多)连续的表槽才能找到它,因为会有很多冲突。但是,如果针对不是成员的对象探测集合,则线性探测将必须检查 每个 表槽,然后才能确定该对象不是成员。对于一个完整的表,大多数探测操作将降级到 O(N) 时间,而大多数基于散列的方法的目标是使操作成为 O(1) 时间。

因此我们有一个类时空权衡。如果我们把桌子做大,整个桌子都会有空槽。存储物品时,应该少碰撞,线性探测会更快找到空槽。彼此相邻的完整插槽集群将更小。对非成员的探测将进行得更快,因为他们更有可能在线性探测时更快地遇到空槽——可能在根本不需要重新探测之后。

在提出实施方案时,我们使用不同的扩展因子运行了一系列基准测试。 (我在代码中使用了术语EXPAND_FACTOR,而大多数文献使用负载因子。原因是扩展因子是负载因子的倒数,如@987654327 中使用的那样@,并且对这两种含义都使用“负载因子”会令人困惑。)当扩展因子接近 1.0 时,探针性能很慢,正如预期的那样。随着膨胀系数的增加,它得到了显着改善。当它达到 3.0 或 4.0 时,改进实际上已经趋于平缓。我们选择 2.0 是因为它获得了大部分性能改进(接近 O(1) 时间),同时与 HashSet 相比提供了良好的空间节省。 (抱歉,我们尚未在任何地方发布这些基准数字。)

当然,所有这些都是实施细节,随着我们找到更好的方法来优化系统,可能会从一个版本到下一个版本发生变化。我确信有一些方法可以改进当前的实现。 (幸运的是,当我们这样做时,我们不必担心preserving iteration order。)

关于开放寻址和性能权衡与负载因子的一个很好的讨论可以在

的第 3.4 节中找到

塞奇威克、罗伯特和凯文·韦恩。 算法,第四版。艾迪生-韦斯利,2011 年。

在线图书网站是here,但请注意印刷版有更多详细信息。

【讨论】:

  • 在所用算法的代码中添加一个简单的注释是否有意义?在我深入研究为什么时,那一行本可以为我节省一些时间。 BTW HashMap 在实现细节中有一堆这样的 cmets...
  • 当你说搜索时,我立刻做出了一个脾气暴躁的表情。我显然是匆忙提出这个问题,并没有考虑其他方面。谢谢
  • 当我们这样做时……这些不可变集合是否故意没有优化的forEach(…)spliterator() 方法?
  • @Stuart 到目前为止,您对线性探测性能以及内存与时间的权衡感到满意吗?我想对于具有几个元素的不可变集合,线性探测根本不会受到伤害,但是对于不可变集合(即 1k、10k 和 100k 元素),您的测试显示了什么?您是否期望这些不可变集合用于如此大量的元素?如果是,那么也许动态探测方案可能会有所帮助......
  • @FedericoPeraltaSchaffner 我认为目前的性能和空间权衡是令人满意的,但它们肯定可以改进。设计中心的元素数量很少,但即使有大量元素,性能也会按预期扩展,仍然是 O(1),尽管比 HashMap 慢。更好的探测会很好,但我更担心在存在错误的hashCode 实现时性能不佳。
猜你喜欢
  • 2016-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 2019-05-22
  • 2023-03-17
  • 2017-12-20
相关资源
最近更新 更多