字符串实习真的很有用吗?

时间:2011-07-11 17:25:55

标签: java .net python ruby string-interning

我前段时间就字符串和各种语言进行了对话,并提出了string interning的主题。显然,Java和.NET框架会自动执行所有字符串以及多种脚本语言。从理论上讲,它节省了内存,因为你最终没有同一个字符串的多个副本,并且它节省了时间,因为字符串相等比较是一个简单的指针比较,而不是通过字符串的每个字符运行的O(N)。

但是我越是想到它,我对这个概念的好处越来越持怀疑态度。在我看来,优势主要是理论上的:

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

这只是我考虑实施细节的结果。有没有我错过的东西?字符串实习在一般情况下实际上是否提供了任何显着的好处?

编辑2:好吧,显然我是在错误的前提下操作的。我正在谈话的人从未指出字符串实习对于新创建的字符串是可选的,事实上给人的印象是相反的情况是正确的。感谢Jon直接解决问题。另一个接受的答案是他。

7 个答案:

答案 0 :(得分:26)

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

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

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

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

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

答案 1 :(得分:6)

  

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

这是真的,字符串在Java中是不可变的。我不确定这是不是坏事。没有进入不可变对可变,我喜欢认为这是一个伟大的设计,因为缓存和更多的简单,我不会接受。

  

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

不完全是O(n)。您可以使用哈希映射和/或其他数据结构来实现近乎不变的查找。

  

如果字符串相等表使用强引用,则字符串将   因此,当它们不再需要时,永远不会收集垃圾   浪费记忆。另一方面,如果表使用弱引用,   然后字符串类需要某种终结器来删除   表中的字符串,从而减慢了GC过程。 (哪可能   非常重要,取决于字符串实习生表的方式   实现。最坏的情况是,从哈希表中删除项目即可   需要在某些情况下对整个表进行O(N)重建   情况。)

你是对的,我同意你的意见。除了我觉得GC处理而且可以忽略不计。从长远来看,这些好处比让垃圾收集器进行额外检查更有用。我不确定你从哈希表中删除O(n)的意思。哈希表上的大多数操作都是O(1)

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

这是关于实际发生的事情以及如何节省内存的nice quote

  

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

答案 2 :(得分: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.

如果你是intern()第一个值并且必须使用intern()一个值来进行比较

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

打印

The average time for equals() was 258 ns.

这是一种常见的情况,因为你经常会有一个你知道被拘留的值,第二个是输入的而不是实习的。

如果你只使用intern'ed Strings和==它,并且不计算成本,打印

The average time for equals() was 4 ns.

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

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

答案 3 :(得分:3)

这是python documentation's对它的看法:

  

sys.intern(string)

     

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

     

实习字符串不是不朽的;你必须保持对intern()的返回值的引用才能从中受益。

答案 4 :(得分:0)

您列出的积分在某种程度上都有效。但是有一些重要的反驳论点。

  1. 不变性非常重要,特别是如果你使用哈希映射,它们会被大量使用。
  2. 无论如何,字符串组合操作非常慢,因为您必须不断重新分配包含字符的数组。
  3. 另一方面,subString()操作非常快。
  4. 确实使用了字符串相等性,并且你不会丢失任何东西。原因是字符串没有自动实现。事实上,如果引用不同,在Java中,equals()会逐字逐句地回到字符。
  5. 显然,对实习生表使用强引用并不是一个好主意。你必须忍受GC开销。
  6. Java字符串处理旨在节省空间,尤其是在常量字符串和子字符串操作上。
  7. 总的来说,我认为在大多数情况下这是值得的,并且与VM管理的堆概念非常吻合。我可以想象一些特殊情况,但这可能是一个真正的痛苦。

答案 5 :(得分:0)

  

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

是。很大。在java中试试。

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

a.equals( b )  is slow

a == b is fast.

答案 6 :(得分:0)

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

然后,能够快速==代替equals()的好处超过了实习字符串的开销。

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