【问题标题】:Helping the JVM with stack allocation by using separate objects通过使用单独的对象帮助 JVM 进行堆栈分配
【发布时间】:2013-11-21 21:19:20
【问题描述】:

我有一个瓶颈方法,它试图将点(作为 x-y 对)添加到 HashSet。常见的情况是集合已经包含在这种情况下什么都不会发生的点。我应该使用一个单独的点来添加我用来检查集合是否已经包含它的点吗?似乎这将允许 JVM 在堆栈上分配检查点。因此,在一般情况下,这不需要堆分配。

例如。我正在考虑改变

HashSet<Point> set;

public void addPoint(int x, int y) {
    if(set.add(new Point(x,y))) {
        //Do some stuff
    }
}

HashSet<Point> set;
public void addPoint(int x, int y){
    if(!set.contains(new Point(x,y))) {
        set.add(new Point(x,y));
        //Do some stuff
    }
}

是否有一个分析器可以告诉我对象是分配在堆上还是栈上?

编辑:为了澄清为什么我认为第二个可能更快,在第一种情况下,对象可能会或可能不会添加到集合中,因此它不是非转义且无法优化。在第二种情况下,分配的第一个对象显然是非转义的,因此它可以由 JVM 优化并放入堆栈。第二次分配只发生在它尚未被包含的极少数情况下。

【问题讨论】:

  • 对象总是在堆上。参考将在堆栈上。
  • 我不明白为什么第二个代码在性能或内存使用方面应该更好!如果尚未包含该点,则您将创建两次。通常,所有对象都在堆上创建(有不同种类的“堆”,这取决于 VM)。只有在逃逸分析的情况下,VM 才可能决定不“真正”创建第一个点。
  • 我认为JVM可以识别非转义对象并将它们分配到堆栈上。请参阅这篇文章:stefankrause.net/wp/?p=64 我正在尝试通过使我的对象不转义来帮助 JVM 退出
  • 是的,但你不应该依赖它。
  • 为什么不呢?我的论点有什么问题?

标签: java optimization jvm stack


【解决方案1】:

Marko Topolnik 正确回答了您的问题;分配给第一个 new Point 的空间可能会或可能不会立即释放,并且指望它发生可能是愚蠢的。但我想详细说明为什么你目前处于罪恶的深渊:

你试图以错误的方式优化它。

您已经确定对象创建是这里的瓶颈。我会假设你是对的。您希望,如果您创建的对象更少,代码会运行得更快。这可能是真的,但它永远不会像您设计的那样运行得非常快。

Java 中的每个对象都有一个相当大的标头(16 字节;一个 8 字节的“标记字”,充满位字段和一个 8 字节指向类类型的指针),并且取决于您的程序到目前为止发生的情况,可能是另一个非常胖的预告片。您的 HashSet 不只是存储对象的内容;它存储指向那些fat-headers-followed-by-contents的指针。 (实际上,它存储了指向 Entry 类的指针,而这些类本身存储了指向 Points 的指针。那里有两个间接级别。)

然后,HashSet 查找会确定它需要查看哪个存储桶,然后在存储桶中为每个事物追逐一个指针以进行比较。 (作为系列中的一个大链。)这些对象可能并不多,但几乎可以肯定它们不会紧密存储在一起,这让你的缓存很生气。请注意,Java 中的对象分配非常便宜——你只需增加一个指针——这很可能是导致速度变慢的更大原因。

Java 没有像 C++ 的模板那样提供任何抽象,因此,要想快速实现这一点并且仍然提供 Set 抽象,唯一真正的方法是复制 HashSet 的代码,更改所有数据结构以表示您的内联对象,修改方法以使用新数据结构,如果您仍然担心,请复制相关方法,这些方法采用与对象内容相对应的参数列表(即contains(int, int))做正确的事情无需构造新对象。

这种方法容易出错且耗时,但不幸的是,在处理性能很重要的 Java 项目时经常需要这种方法。看看 Marko 提到的 Trove 库,看看是否可以使用它; Trove 正是为原始类型做到了这一点。

除此之外,单态调用站点是一个只调用一个方法的站点。 Hotspot 积极地内联来自单态调用站点的调用。你会注意到HashSet.contains 平底船到HashMap.containsKey。你最好祈祷 HashMap.containsKey 被内联,因为你需要内部的 hashCode 调用和 equals 调用是单态的。您可以通过使用 -XX:+PrintAssembly 选项并仔细研究输出来验证您的代码是否被很好地编译,但它可能不是——即使是这样,它可能仍然很慢,因为 HashSet 是什么。

【讨论】:

  • 对象头只有 8 或 16 个字节。
  • @leventov:哎呀,是的。你是对的,开销并不都是标题。我修好了。 (但是:为了在 gc(以及锁定等)期间保留哈希码,JVM 将标记字中的 hashCode 字段设置为“查看对象”(如果它需要该空间)。另外,如果你已经使用了对象的监视器(synchronizedwaitnotify 等),正常的标题已被重新定位到其他地方并替换为指向监视器的指针。所有这些,包括监视器,加起来相当比我最初声称的 6 或 7 个单词多一点,尽管它们没有存储在标题中。)
  • 1) 在 32 位 VM 上只有 8 个字节。 2)当我们在Java中讨论类结构类时,我们通常会覆盖hashCode(),这样它就不会存储在对象头中,也不会锁定它的实例。
  • @leventov: 1) 现在是 2013 年;没有人应该仍然使用 32 位 Java。 2) 身份哈希码仍然存在,并且必须在 GC 中保持一致。您似乎试图让我在这里进行最佳案例分析 --- 我们 可能 只需要 8 个字节 如果 您运行 32 位 JVM 并且 GC 永远不会运行并且 你永远不会在对象上同步。虽然 OP 建议使用 Point 几乎可以肯定不涉及在 Point 上进行同步,但我们不能忽视不合时宜的垃圾收集或假设 GC 会立即注意到我们的临时 Point 是垃圾——我们必须占空间。
  • Point 类必须是 1) 不可变的,从不需要同步。由于有Java OO 设计的经验(记得java.awt.Dimension?)无论如何,我们不能修改添加到集合或映射中的对象。 2)hashCode() 定义为xCoord() ^ yCoord()不是默认 hashCode() impl,如果 OP 考虑构造单独的对象以插入集合,不是吗?
【解决方案2】:

一旦你写了new Point(x,y),你就创建了一个新对象。它可能碰巧没有放在堆上,但这只是你可能输掉的赌注。例如,contains 调用应该被内联以使转义分析起作用,或者至少它应该是一个单态调用站点。所有这一切都意味着您正在针对一个非常不稳定的性能模型进行优化。

如果您想避免以固定方式分配,您可以使用 Trove 库的 TLongHashSet 并将您的 (int,int) 对编码为单个 long 值。

【讨论】:

  • “点”实际上只是一个例子。我的对象比这稍微复杂一些。我意识到可能有更好的方法,但我想知道我的论点是否合理。什么是“单态调用站点”?我可以在哪里阅读更多相关信息?
  • Google 不会让您失望的。
猜你喜欢
  • 1970-01-01
  • 2012-08-01
  • 2013-01-25
  • 1970-01-01
  • 1970-01-01
  • 2017-02-21
  • 2018-08-11
  • 1970-01-01
  • 2019-01-12
相关资源
最近更新 更多