【问题标题】:Why is string.intern() so slow?为什么 string.intern() 这么慢?
【发布时间】:2011-11-07 23:15:43
【问题描述】:

在任何人质疑使用string.intern() 的事实之前,让我说,出于内存和性能原因,我在我的特定应用程序中需要它。 [1]

所以,到目前为止,我一直使用String.intern(),并认为这是最有效的方法。但是,我注意到自古以来它就是软件的一个瓶颈。 [2]

然后,就在最近,我尝试用一​​个巨大的地图替换String.intern(),我在其中放置/获取字符串,以便每次都获得一个唯一的实例。我预计这会更慢......但事实恰恰相反!它的速度非常快!通过推送/轮询地图(实现完全相同)替换intern(),速度提高了一个数量级以上。

问题是:为什么intern() 这么慢?!?为什么它不简单地由地图(或者实际上,只是一个定制的集合)支持并且会非常快?我很困惑。

[1]:对于不相信的人:它属于自然语言处理,必须处理千兆字节的文本,因此需要避免同一字符串的多个实例以避免炸毁内存和引用字符串比较足够快.

[2]:没有它(普通字符串)是不可能的,有了它,这个特定的步骤仍然是最密集的计算步骤

编辑:

由于对这篇文章的惊人兴趣,这里有一些代码来测试它:

http://pastebin.com/4CD8ac69

还有超过 100 万个字符串的实习结果:

  • HashMap:4 秒
  • String.intern():54 秒

为了避免一些预热/操作系统 IO 缓存和类似的东西,通过颠倒两个基准的顺序重复实验:

  • String.intern():69 秒
  • HashMap:3 秒

如您所见,差异非常明显,超过十倍。 (使用 OpenJDK 1.6.0_22 64bits ...但我认为使用 sun 的结果相似)

【问题讨论】:

  • 好问题,你能给出一些关于intern() 慢多少的指标吗?
  • 你最终分配了多少个字符串实例到地图/实习?
  • 您是将intern 与同步的Hashtable 还是未同步的HashMap 进行比较? intern 是同步的,所以这可能是您所看到的大部分内容。
  • 什么样的字符串操作? (例如子字符串提取与删除/编辑与连接)您可能想查看绳索。我也很好奇你是如何得到字符串的:当你做 readline() 或其他什么时,字符串是 equal() 不同的对象吗?!
  • 你的实现线程安全吗?它是否使用弱引用?

标签: java string performance


【解决方案1】:

我不能从任何伟大的经验中说出来,但来自字符串文档:

"当调用实习生方法时,如果池中已经包含一个字符串,该字符串等于由 {@link #equals(Object)} 方法确定的此String 对象,则返回池中的字符串。否则,这个String 对象被添加到池中,并返回对这个String 对象的引用。"

在处理大量对象时,任何涉及散列的解决方案都将优于不涉及散列的解决方案。我认为您只是看到了滥用 Java 语言功能的结果。实习不是作为供您使用的字符串映射。您应该为此使用 Map(或 Set,视情况而定)。 String 表用于语言级别的优化,而不是应用级别的优化。

【讨论】:

  • 但是内置的实习生方法不会使用映射来确定池是否已经包含字符串? (如果不是,这似乎是一个可以改进的明显低效率)
  • "Interning 不能作为字符串映射供您使用。" String.intern 是公开的,因此可以说它供您使用。也许 OP 使用不正确,但您的回答并没有真正表明他的用法有什么不正确。
  • HashMap 也使用 equals(),我赌 100 美元 intern() 最终会使用某种哈希表。
  • @Michael Borgwardt 你应该更像一个赌徒:) 刚刚查看了热点 vm 中的 .cpp,它肯定使用了某种形式的哈希表。
  • @John:那你为什么认为 intern() 表现如此糟糕?您是否在源代码中找到了相关信息?
【解决方案2】:

性能差异的最可能原因:String.intern() 是本机方法,调用本机方法会产生大量开销。

那么为什么它是本地方法呢?可能是因为它使用了常量池,这是一种低级 VM 构造。

【讨论】:

  • 我认为 JVM 级别的对象池会更快。
  • @Arnaud:一点也不。在 Java 的早期,当 JVM 非常非常慢时,出于性能原因使用本机方法可能是有保证的。但这种情况在 10 多年前发生了变化。现在几乎使用 JNI 的唯一原因是访问不属于 Java 标准 API 的功能。
  • 我可以肯定地告诉你,它慢的原因不是因为它是一种本机方法。如果这就是问题所在,那么无论字符串池的大小如何,您都希望付出相同的性能损失(跨越 JVM 原生障碍)。我的基准测试 (stackoverflow.com/questions/10624232) 表明 String.intern() 的复杂性在 O(n^2) 中,其中 n 是池中的字符串数。这个问题的真正答案是:String.intern 使用的算法很糟糕,无法扩展。
  • @MichaelBorgwardt 在这种情况下,我认为 JNI 用于对字符的其他静态内存位置进行魔术。本机方法可以随意更改字段。我认为 intern() 根本不应该用于性能优化;充其量是对内存使用的优化。可能它可能会在很长的字符串中对equals 产生影响(你为什么要这样做),否则影响可以忽略不计。
  • 也许你错过了这个:“标准库中的本地方法不一定要通过 JNI”。 JNI 很贵。调用作为 JDK 一部分的本机方法非常便宜。
【解决方案3】:

@Michael Borgwardt 在评论中这样说:

intern() 不同步,至少在 Java 语言级别。

我认为你的意思是 String.intern() 方法没有在 String 类的源代码中声明为 synchronized 。确实,这是一个真实的陈述。

但是:

  • intern() 声明为synchronized 只会锁定当前的String 实例,因为它是实例方法,而不是静态方法。所以他们无法以这种方式实现字符串池同步。

  • 如果你退一步想一想,字符串池必须执行某种内部同步。如果不是这样,它在多线程应用程序中将无法使用,因为对于所有使用intern() 方法进行外部同步的代码根本没有实用的方法。

因此,字符串池执行的内部同步可能成为大量使用intern()的多线程应用程序的瓶颈。

【讨论】:

  • 仅供参考,我使用法线和同步地图测试了代码。性能差异可以忽略不计。
  • @arnaud - 是的。如果没有争用,那么在 Map 上同步的成本可以忽略不计。当存在重大争用时会出现性能问题;即许多不同的线程试图同时使用地图。
  • intern() 绝对是同步的,这可能是性能差异的主要因素。 intern() 必须绝对确保只有一个版本的 String 进入实习池,并且该池从各个角度受到冲击——多个线程、正在加载的类、处理字符串的本机方法。同步问题相当复杂。
【解决方案4】:

这个article 讨论了String.intern() 的实现。在 Java 6 和 7 中,实现使用了固定大小 (1009) 的哈希表,因此随着条目数的增加,性能变为 O(n)。可以使用-XX:StringTableSize=N 更改固定大小。显然,在 Java8 中,默认大小更大,但问题仍然存在。

【讨论】:

    猜你喜欢
    • 2021-09-03
    • 2016-09-28
    • 2020-02-08
    • 2012-07-17
    • 2015-08-24
    • 2013-08-06
    • 2014-07-16
    • 2011-01-02
    • 2019-06-16
    相关资源
    最近更新 更多