【问题标题】:Is string interning really useful?字符串实习真的有用吗?
【发布时间】:2011-10-02 23:53:29
【问题描述】:

前段时间我在讨论字符串和各种语言,string interning 的话题出现了。显然,Java 和 .NET 框架会自动对所有字符串以及几种脚本语言执行此操作。从理论上讲,它可以节省内存,因为您最终不会得到同一个字符串的多个副本,并且它可以节省时间,因为字符串相等比较是一个简单的指针比较,而不是 O(N) 遍历字符串的每个字符。

但我想得越多,就越怀疑这个概念的好处。在我看来,优势主要是理论上的:

  • 首先,要使用自动字符串暂留,所有字符串都必须是不可变的,这使得很多字符串处理任务比实际需要的更难。 (是的,我听过所有关于不变性的论点。这不是重点。)
  • 每次创建新字符串时,都必须根据字符串实习表检查它,这至少是一个 O(N) 操作。 (编辑:其中 N 是字符串的大小,而不是表的大小,因为这让人们感到困惑。)因此,除非字符串相等比较与新字符串创建的比率相当高,否则它是节省的净时间不太可能是正值。
  • 如果字符串相等表使用强引用,则字符串在不再需要时永远不会被垃圾回收,从而浪费内存。另一方面,如果表使用弱引用,则字符串类需要某种终结器来从表中删除字符串,从而减慢 GC 过程。 (这可能非常重要,具体取决于字符串实习生表的实现方式。在最坏的情况下,从哈希表中删除一个项目可能需要在某些情况下对整个表进行 O(N) 次重建。)

这只是我思考实现细节的结果。有什么我错过的吗?在一般情况下,字符串实习是否真的提供了任何显着的好处?

编辑 2: 好吧,显然我是从一个错误的前提出发。与我交谈的人从未指出弦乐实习对于新创建的弦乐是可选的,实际上给人的强烈印象是相反的情况。感谢 Jon 把事情弄清楚了。另一个接受他的答案。

【问题讨论】:

  • 为什么你认为根据字符串实习表检查一个新字符串是一个 O(N) 操作?
  • 有趣的问题。我不同意 O(N),因为实习生表可以是字典。
  • Java 并非对所有字符串都这样做 - 只是所有字符串 literals,可以在编译时确定并设置为类加载的一部分,因此几乎没有运行时间成本。新的 String 对象没有被实习;代码必须显式调用它们的 intern() 方法才能这样做。因此,您的代码可以决定实习是否适合其使用模式,并选择是否使用它。实习字符串池不算作强引用,因此不排除 GC。
  • 我有一种感觉,很难说实习和不变性是鸡还是蛋。使字符串不可变是有原因的,这种实现的有用好处之一可能是实习,但这可能不是主要原因。
  • “O(N) 操作。(编辑:其中 N 是字符串的大小,而不是表格的大小,因为这让人感到困惑。)”。令人困惑是有原因的。字符串的长度很少适用于实习字符串,因为哈希只计算一次。大小无关紧要。

标签: java .net python ruby string-interning


【解决方案1】:

不,Java 和 .NET 不会“自动处理所有字符串”。他们(好吧,Java 和 C#)使用以字节码/IL 表示的 constant 字符串表达式,并通过 String.internString.Intern (.NET) 方法按需执行。 .NET 中的确切情况很有趣,但基本上 C# 编译器将保证程序集中对相等字符串常量的每个引用最终都引用相同的字符串对象。这可以在类型初始化时有效地完成,并且可以节省大量内存。

不会在每次创建新字符串时发生。

(在字符串不变性方面,我非常很高兴字符串是不可变的。我不想每次收到参数等时都复制一份,非常感谢很多。我也没有看到它使字符串处理任务更难......)

正如其他人所指出的,在哈希表中查找字符串通常不是 O(n) 操作,除非您非常不幸地遇到哈希冲突...

就我个人而言,我不在用户端代码中使用字符串实习;如果我想要某种字符串缓存,我会创建一个HashSet<string> 或类似的东西。这在您希望多次遇到相同字符串(例如 XML 元素名称)但使用简单集合不会污染系统范围缓存的各种情况下很有用。

【讨论】:

  • 为了提供一些观点,我习惯了 Delphi,其中字符串是一种引用类型,编译器保证了引用计数和写时复制语义。作为参数传递时无需复制;它仅在您修改字符串时复制。如果您将其作为 const 参数传递,您甚至可以跳过引用计数开销。
  • @Mason:引用计数当然有它自己的麻烦,比如循环......无论如何,你问题中的大多数断言都是不正确的。
  • @Mason Wheeler 我在 Delphi 中编码了几年,但我不记得那里有这种行为。据我所知,字符串只是数组 + 长度计数器。
  • @Mason Wheeler 好吧,这只是意味着 Delphi 为您创建了不可变的字符串,但是用一层厚厚的语法糖覆盖了它。
  • Java 中可变字符串对象的引用计数或写入时复制将很困难 - 我认为引用计数(实际上是所有字符串分配)和变异操作必须同步以避免损坏多线程访问。对于基本类型来说,这是很高的开销。不变性意味着引用可以跨线程共享而无需锁定。
【解决方案2】:

首先,要使用自动字符串实习,所有字符串都必须是 不可变,这使得很多字符串处理任务比 他们需要。 (是的,我听过所有关于 一般的不变性。这不是重点。)

这是真的,字符串在 Java 中是不可变的。我不确定这是否是一件坏事。在不涉及不可变与可变的情况下,我喜欢认为这是一个很棒的设计,因为缓存和更多的简单性,我不会进入。

每次创建新字符串时,都必须根据 字符串实习表,这至少是一个 O(N) 操作。所以除非 字符串相等比较与新字符串创建的比率是 相当高,节省的净时间不太可能是积极的 价值。

不完全是 O(n)。你可以做哈希图和/或其他数据结构,这将使它接近于不断的查找。

如果字符串相等表使用强引用,字符串将 当不再需要垃圾时,永远不要收集垃圾,因此 浪费内存。另一方面,如果表使用弱引用, 那么字符串类需要某种终结器来删除 表中的字符串,从而减慢 GC 过程。 (这可以 非常重要,具体取决于字符串实习生表的方式 实施的。最坏的情况,从哈希表中删除一个项目可以 需要在某些情况下对整个表进行 O(N) 重建 情况。)

你是对的,我同意你的看法。除了我觉得GC处理和微不足道。从长远来看,这比让垃圾收集器进行额外检查更有用。我不确定从哈希表中删除 O(n) 是什么意思。哈希表上的大多数操作都是 O(1)

总而言之,我认为您的假设是大多数操作都是线性的。但是查找字符串更接近于一个恒定的时间。因此,这种方法的性能损失可以忽略不计,但内存增益很大。我认为这是值得的。

这里是nice quote,介绍实际发生的情况以及它如何节省内存。

为了节省内存(并加快相等性测试),Java 支持 字符串的“实习”。当 intern() 方法被调用时 字符串,对实习字符串表执行查找。如果一个 具有相同内容的字符串对象已经在表中,一个 返回对表中字符串的引用。否则,该 字符串被添加到表中并返回对它的引用。

【讨论】:

  • 问题是“字符串实习真的有用吗?”。您的回答并没有真正回答问题,看起来像是扩展评论。
  • 我还在编辑。但有我的答案。忽略cpu损失与大内存增益。投票是有用的。
  • 不要认为有真正的内存增益。只有字符串文字才能到达实习生表。如果我在代码中重复了字符串值,我会将它们提升为基本相同的常量。字符串的不变性会用短的离开对象污染堆,所以我认为在性能方面没有真正的好处。
  • 不确定您的意思,因为如果您进行大量字符串操作并且虚拟机中只有该字符串的一个副本,那么我觉得会增加内存。 wiki 还引用了“Interning strings 使一些字符串处理任务更加节省时间或空间”。你是说收益并没有人们想象的那么多吗?
  • “我不确定你对从哈希表中删除 O(n) 是什么意思。对哈希表的大多数操作都是 O(1)” 大多数操作,是的。但是,如果您有两个散列到表中相同位置的键,并且冲突解决涉及将这两个键中的一个放在其他位置,然后删除正确位置的键,则现在会中断另一个键的查找,除非你重述它。这通常涉及重建整个表。
【解决方案3】:

这是 python documentation's 承担它:

sys.intern(string)

在“interned”字符串表中输入 string 并返回 interned 字符串 - 它是字符串本身或副本。实习字符串 对于在字典查找中获得一点性能很有用——如果 字典中的键是实习的,查找键是实习的, 键比较(散列后)可以通过指针比较来完成 而不是字符串比较。通常,Python 中使用的名称 程序被自动实习,而字典用来保存 模块、类或实例属性具有内部键。

内部字符串不是不朽的;您必须保留对 intern() 的返回值的引用才能从中受益。

【讨论】:

    【解决方案4】:

    a.equals(b) 对于随机字符串非常快。它只对长且相同(或几乎相同)的字符串很慢

    Random rand = new Random(1);
    String[] list = new String[2000];
    for(int i=0;i<list.length;i++)
        list[i] = "1234567"+Long.toString(rand.nextInt(36*37), 36); // semi random
    int count = 0;
    long start = System.nanoTime();
    for(int i=0;i<list.length;i++)
        for(int j=0;j<list.length;j++)
            if (list[i].equals(list[j]))
                count++;
    long time = System.nanoTime() - start;
    System.out.printf("The average time for equals() was %,d ns.%n", time/list.length/list.length);
    

    在 2.3 GHz 笔记本电脑上打印

    The average time for equals() was 19 ns.
    

    如果你实习()第一个值并且必须实习()一个值来进行比较

           if (list[i] == list[j].intern())
    

    打印

    The average time for equals() was 258 ns.
    

    这是一种常见的情况,因为您经常有一个值您知道是已实习的,而第二个值是输入但未实习的。

    如果您只使用实习字符串并 == 它,并且不计算成本,则打印

    The average time for equals() was 4 ns.
    

    如果您进行数百万次比较,这会快很多倍。但是,对于少数比较,您节省了 8 ns,但可能会多花费 250 ns。

    避免使用 intern() 并使用 equals() 可能更简单。

    【讨论】:

    • 好点。每次都必须做实习生才能在等号检查上“保存”是不行的。仅当您需要一个具有大量读取的映射并且您完全控制键时,实习才是明智之举……在这种情况下,您可能只需 == 对它们进行操作,而无需填写实习表。
    • ...或者如果你的瓶颈是内存并且你有很多重复的字符串。在这种情况下,花费更多的 cpu 来保留工作内存会在用户体验方面得到回报……但是,这是一个极端情况,不应该影响一般使用。
    【解决方案5】:

    你列出的点在一定程度上都是有效的。但也有重要的反驳。

    1. 不变性非常重要,尤其是在您使用哈希映射时,并且它们被大量使用。
    2. 无论如何,字符串组合操作都很慢,因为您必须不断地重新分配包含字符的数组。
    3. 另一方面,subString() 操作非常快。
    4. 字符串相等确实被使用了很多,而且你不会在那里丢失任何东西。原因是字符串不会自动实习。事实上,在 Java 中,如果引用不同,equals() 会回退到逐字符比较。
    5. 显然,对实习生表使用强引用并不是一个好主意。您必须忍受 GC 开销。
    6. Java 字符串处理旨在节省空间,尤其是在常量字符串和子字符串操作上。

    总的来说,我认为在大多数情况下它是值得的,并且非常适合 VM 管理的堆概念。我可以想象一些特殊的场景,但它可能是一个真正的痛苦。

    【讨论】:

    • substring 在 java 7 上的速度较慢... java6 和 lower 返回一个指向原始字符串的 char[] 的字符串对象(因此会泄漏内存)。 7 现在也为子字符串制作不可变的数组副本;这是更多的运行时数据,但它减少了内存。 Intern() 是一回事;让 == 得到回报很难(两个字符串都必须被实习),但如果你有 2^20 个字符串,实习将节省你的堆,并且在苛刻的情况下会有更高的性能。
    【解决方案6】:

    字符串实习在一般情况下是否真的提供了任何显着的好处?

    是的。很大。用java试试吧。

    编写简单的测试,比较 1,000 个半随机字符串是否相等,有无实习。

    a.equals( b )  is slow
    
    a == b is fast.
    

    【讨论】:

    • 是的,但这就是我的意思。有几种字符串操作,其中相等比较是唯一受益于此的操作。您多久使用一次字符串相等比较?
    • @Mason Wheeler:不断。事实上,我很少使用其他任何东西。 “排序”比较少见,我尽量在设计上尽量避免。
    • a.equals(b) 对于随机字符串非常快,它首先比较长度,然后比较第一个字符。对于随机字符串,如果它们不同,则需要查看。
    • @Peter Lawrey:因此建议使用“半随机”字符串。我们使用 20,000 个金融账户进行了比较,这些账户有 8 个或 9 个字符,有很多不同长度的重复模式。 “随机”不是用于测试任何东西的真实数据。
    • @S. Lott,我进行了性能测试并比较了 8-9 个字符的半随机字符串,使用 == 与 equals 相比节省了 15 ns,但使用 intern() 成本为 250 us。
    【解决方案7】:

    当您需要多次比较有限集 (2) 中的字符串 (1) 时,字符串实习很有用。

    然后,可以快速执行== 而不是equals() 的好处超过了实习字符串的开销。

    这样做有时会比使用HashMap 更快,后者依赖于hashCode()equals() 调用。

    【讨论】:

      猜你喜欢
      • 2011-02-11
      • 2015-02-26
      • 2013-03-10
      • 2013-03-16
      • 2010-09-23
      • 1970-01-01
      • 2016-09-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多