【问题标题】:Java 7 String - substring complexityJava 7 字符串 - 子字符串复杂度
【发布时间】:2013-04-13 22:59:42
【问题描述】:

在 Java 6 之前,我们在 String 上有一个恒定时间子字符串。在 Java 7 中,为什么他们决定复制 char 数组 - 并降低到线性时间复杂度 - 而像 StringBuilder 这样的东西正是为此而生的?

【问题讨论】:

  • 为了避免字符串长度过小,防止垃圾回收任意大的char[]
  • 使用StringBuilder应该可以解决这样的问题吧?
  • 使用StringBuilder 可让您在发现问题存在后解决该问题。但它并不能修复现有代码中的内存泄漏。此更改修复了现有代码中的内存泄漏,并且由于缓冲区副本通常是硬件支持的,因此最终不会为任何适合一个虚拟内存页面的子字符串花费线性时间。

标签: java java-7


【解决方案1】:

这只是他们修复一些 JVM 垃圾收集限制的糟糕方法。

在 Java 7 之前,如果我们想避免垃圾回收不工作的问题,我们总是可以复制子字符串而不是保留子字符串引用。这只是对复制构造函数的额外调用:

String smallStr = new String(largeStr.substring(0,2));

但是现在,我们不能再有一个恒定时间的子字符串了。真是一场灾难。

【讨论】:

  • 这完全正确。许多类型的程序都受益于共享子字符串的使用。编译器和解析器很好地说明了受到最大伤害的类型操作:但伤害远远超出了那些特定类型的程序。
  • 是否有人知道任何具有自定义实现的 CharSequence(或类似的东西)复制“旧”子字符串行为的第三方库/代码?我经常需要处理类似 CSV 的大型文件 (500+MB),每当我分析它们时,我意识到至少 10% 的处理时间似乎浪费在调用 Arrays.copyOfRange() 上。
  • @SimonBerthiaume 性能错误是首先创建String 实例,即使在调用substring 之前,它已经承担了不必要的复制操作。由于每个CharsetDecoder,包括那些封装在Reader 中的@,都在CharBuffer 上运行,这就是您的起点。它已经是解决方案,因为它实现了CharSequence,因此您可以将其传递给正则表达式模式匹配引擎等工具,并且可以免费复制subSequenceslice 操作。您只需要创建最终匹配结果字符串。即使是简单的java.util.Scanner 也可以这样工作
  • 现在我们可以使用subSequence(startIndex: Int, endIndex: Int): CharSequence
【解决方案2】:

我相信,主要动机是String 和它的char[] 最终“共同定位”。现在它们定位在远处,这是对缓存行的主要惩罚。如果每个String 都拥有自己的char[],JVM 可以将它们合并在一起,读取速度会快很多。

【讨论】:

    【解决方案3】:

    这将对后缀数组等数据结构的复杂性产生相当大的影响。 Java 应该提供一些替代方法来获取原始字符串的一部分。

    【讨论】:

      【解决方案4】:

      Oracle bug #4513622 : (str) keeping a substring of a field prevents GC for object 讨论了他们决定的原因:

      在示例中调用 String.substring 时,不会分配用于存储的新字符数组。它使用原始字符串的字符数组。因此,支持原始字符串的字符数组不能被 GC,直到子字符串的引用也可以被 GC'd。这是一个有意的优化,以防止在常见场景中使用子字符串时过度分配。不幸的是,有问题的代码遇到了原始数组的开销很明显的情况。很难针对两种边缘情况进行优化。空间/尺寸权衡的任何优化通常都很复杂,并且通常可能是特定于平台的。

      还有这个note,指出根据测试,曾经的优化变成了悲观:

      长期以来,我们一直在准备和计划从 java.lang.String 中删除 offset 和 count 字段。这两个字段使多个 String 实例能够共享相同的支持字符缓冲区。共享字符缓冲区是旧基准测试的重要优化,但对于当前现实世界的代码和基准测试,实际上最好不共享后备缓冲区。共享 char 数组后备缓冲区只有在大量使用 String.substring 时才会“获胜”。负面影响的情况可能包括解析器和编译器,但目前的测试表明,这种变化总体上是有益的。

      【讨论】:

        【解决方案5】:

        如果您有一个短寿命的大父字符串的长寿命小子字符串,则支持父字符串的大 char[] 在小子字符串超出范围之前将无法进行垃圾收集。这意味着子字符串会占用比人们预期更多的内存。

        Java 6 唯一一次表现得更好是有人从一个大的父字符串中取出一个大的子字符串,这是一种非常罕见的情况。

        很明显,他们认为这种更改带来的微小性能成本被旧方法引起的隐藏内存问题所抵消。决定因素是问题被隐藏了,而不是有解决方法。

        【讨论】:

        • trim() 从一个大的父字符串中获取一个大的子字符串,并且一直使用。
        • 由于这种(糟糕的..)设计决策而导致算法性能受损是常见的,而不是罕见的。
        猜你喜欢
        • 2015-09-18
        • 2018-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-10
        • 1970-01-01
        • 2013-07-04
        相关资源
        最近更新 更多