【问题标题】:Why only literal strings saved in the intern pool by default?为什么默认情况下只保存在实习生池中的文字字符串?
【发布时间】:2012-01-20 11:54:46
【问题描述】:

为什么默认情况下只有文字字符串保存在实习池中?

来自MSDN的示例:

String s1 = "MyTest";
String s2 = new StringBuilder().Append("My").Append("Test").ToString(); 
String s3 = String.Intern(s2); 
Console.WriteLine("s1 == '{0}'", s1);
Console.WriteLine("s2 == '{0}'", s2);
Console.WriteLine("s3 == '{0}'", s3);
Console.WriteLine("Is s2 the same reference as s1?: {0}", (Object)s2==(Object)s1); 
Console.WriteLine("Is s3 the same reference as s1?: {0}", (Object)s3==(Object)s1);

/*
This example produces the following results:
s1 == 'MyTest'
s2 == 'MyTest'
s3 == 'MyTest'
Is s2 the same reference as s1?: False
Is s3 the same reference as s1?: True
*/

【问题讨论】:

  • 为什么要对每个字符串进行实习?
  • @BoltClock,为什么要对每个文字字符串进行实习?
  • interned 的主要原因是为了节省内存空间,否则会因为在整个程序中多次重复相同的字符串而浪费,更不用说它很容易做到这一点。但我想不出非文字字符串被实习的理由。

标签: c# .net string compiler-construction clr


【解决方案1】:

简短的回答:实习文字字符串在运行时很便宜并且节省内存。实习非文字字符串在运行时很昂贵,因此节省了少量内存,以换取使常见情况变慢的代价。

interning-strings-at-runtime“优化”的成本并没有带来收益,因此实际上并不是优化。实习文字字符串的成本很便宜,因此确实为收益付出了代价。

我在这里更详细地回答你的问题:

http://blogs.msdn.com/b/ericlippert/archive/2009/09/28/string-interning-and-string-empty.aspx

【讨论】:

  • 谢谢!你在那里找到的优秀博客... =) 也许你可以回答一个关于 string.Empty 的related question?
  • 实习文字字符串是否有 ANY 运行时成本?据我了解,如果六个程序集每个都至少使用一次文字字符串“Quack”,那么将创建字符串“Quack”的六个实例——每个程序集一个,运行时无需检查它们的内容。由于反射,.net 通常必须假设始终可以执行任何函数,因此可以访问任何文字字符串,因此文字字符串的生命周期将是它出现的程序集的生命周期。我认为“运行时便宜”是轻描淡写的说法。
  • @supercat:你的理解是正确的;文字字符串实习是在每个程序集的基础上完成的。至于你关于这样一个字符串的生命周期的问题,我不知道我脑海中的答案。我可以假设运行时在没有对它的实时引用时释放这样的字符串,并在以后重新构建它;你怎么能说参考是不同的?但它可以很容易地让它永远活着。
  • @gdoron:我其实不知道这个问题的答案;我自己也想知道同样的事情。我今天要和一些 CLR 的老前辈共进午餐;我看看有没有人记得。我很高兴你喜欢这个博客。
  • 天哪。我认为我的编译器中存在一个潜在的错误——我正在使用 Symbol 结构,它只是 string 的不可变包装器,它调用 Intern 并保留结果——并使用引用比较而不是字符串比较。是否手动调用 Intern 在两个预先在程序集中不同的字符串上保留两个不同的副本?
【解决方案2】:

语言设计者认为实习每个中间字符串值的成本不值得性能成本。垃圾回收字符串的驻留需要一个全局弱映射,当您拥有大量线程时,这可能会成为瓶颈。

【讨论】:

  • @EricLippert:实习字符串不需要全局弱映射。如果每个字符串都包含对具有相同内容的字符串的初始空白引用,该字符串在某些度量(可能是年龄)上排名更高,那么可以通过追踪对最高排名的等效字符串的引用来比较字符串。具有相同“最佳”等效项的两个字符串可以被视为相等,而无需检查它们的内容;否则,如果检查两个字符串并发现它们相等,则可以将“等价”字符串中的一个设置为另一个的等价。
  • @EricLippert:对于这样的设计,为了避免内存泄漏,可能需要有 GC 支持(这样如果一个字符串引用了一个更好的字符串,任何“更好的字符串”引用标识它将被更改以识别更好的)但是这样的设计可以在许多使用场景中提供显着的好处而无需 any 类型的哈希映射,并且可以很好地与每个线程哈希映射一起工作(注意如果发现x 和y 相等,并且a 和b 同样,发现x 等于a 将意味着它等于b,并且y 是等于a 和b。
  • @EricLippert:因此,如果每个线程都有自己的哈希映射,则创建字符串“Fred”的两个线程将开始持有单独的引用,但两者都将被实习到同一个后备存储中将一个线程中的任何“Fred”与另一个线程中的任何“Fred”进行比较。
  • @supercat,Interning 必须是显式的,因为 Java 语言要求 new X() 与之前存在的所有引用不同,并且 string 具有公共构造函数。
  • @MikeSamuel:显式暴露引用的实习必须是显式的,但对象持有对既不修改也不暴露给外部代码的后备存储的引用,那些后备商店可以被隐式地实习。例如,在给定String A="foo", B=new String(A); 的Java 中,B 将是一个新的String 对象,但A 和B 都将持有对相同char[] 的引用。有趣的是,即使两个不同的字符串对象可以...
【解决方案3】:

在大多数字符串使用场景中,即使有一个零成本的弱引用实习池(理想的实习实现),实习字符串也几乎没有任何好处。为了让字符串实习提供任何好处,有必要将多个对巧合相等的字符串的引用保持相当“长”的时间。

考虑以下两个程序:

  1. 从文本文件中输入 100,000 行,每行包含一些任意文本,然后输入 100,000 个五位数。将读入的每个数字视为读入的 100,000 行列表中从零开始的索引,并将相应的行输出到输出。
  2. 从文本文件中输入 100,000 行,输出包含字符序列“fnord”的每一行。

对于第一个程序,根据文本文件的内容,字符串实习可能会节省近 50,000:1 的内存(如果该行包含 100,000 个相同的长文本行)或可能表示完全浪费(如果所有100,000 行不同)。在没有字符串暂留的情况下,具有 100,000 行相同行的输入文件将导致同一字符串的 100,000 个实时实例同时存在。使用字符串实习,活动实例的数量可以减少到两个。当然,编译器甚至无法猜测输入文件是否倾向于包含 100,000 条相同的行、100,000 条不同的行,或者介于两者之间。

对于第二个程序,即使是理想的字符串实习实现也不太可能提供太多好处。即使输入文件的所有 100,000 行恰好相同,实习也无法节省太多内存。实习的效果不是防止创建冗余字符串实例,而是允许识别和丢弃冗余字符串实例。由于每行一旦被检查并且无论是否输出都可以被丢弃,因此实习生唯一可以购买的就是(理论上)能够(非常)比其他方式更早地丢弃冗余字符串实例。

在某些情况下,缓存某些“中间”字符串结果可能会带来好处,但这是最好留给程序员的任务。例如,我有一个程序需要将大量字节转换为两位数的十六进制字符串。为方便起见,我有一个包含 255 个字符串的数组,其中包含从 00 到 FF 的值的字符串等价物。我知道,平均而言,该数组中的每个字符串至少会被使用数百或数千次,因此缓存这些字符串是一个巨大的胜利。另一方面,字符串只能被缓存,因为我知道它们代表什么。我可能知道,对于任何n 0-255,String.Format("{0:X2}",n) 将始终产生相同的值,但我不希望编译器知道这一点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-16
    • 1970-01-01
    • 1970-01-01
    • 2013-01-06
    • 1970-01-01
    • 2022-06-23
    • 2014-11-28
    相关资源
    最近更新 更多