字符串常量池

11
在下面的代码中,我正在检查对象引用的相等性。
string x = "Some Text";
string y = "Some Other Text";
string z = "Some Text";

Console.WriteLine(object.ReferenceEquals(x, y)); // False
Console.WriteLine(object.ReferenceEquals(x, z)); // True
Console.WriteLine(object.ReferenceEquals(y, z)); // False

y = "Some Text";

Console.WriteLine(object.ReferenceEquals(x, y)); // True
Console.WriteLine(object.ReferenceEquals(x, z)); // True
Console.WriteLine(object.ReferenceEquals(y, z)); // True

这里:

  • xz 引用同一个对象;我可以说 x 是内部化的,而 z 则使用了那个版本。嗯,我不确定这是否正确;如果我错了,请纠正我。
  • 我通过将与 x 相同的值分配给 y 来更改了 y 的值。我认为它会在这里创建一个新对象;但是我错了,它使用了相同的引用。

我的问题是:

  • .net 是否对我使用的每个字符串都使用字符串内部化
  • 如果是,那么这难道不会影响性能吗?
  • 如果不是,那么以上示例中引用是如何变成相同的?

string intern 的好文章。 - V4Vendetta
4个回答

16
是的,编译器中的常量字符串表达式会使用 ldstr 进行处理,并通过 MSDN 确保了字符串的联接:

公共语言基础设施 (CLI) 保证了两个 ldstr 指令的结果,它们引用具有相同字符序列的两个元数据标记,将返回完全相同的字符串对象(这个过程称为“字符串联接”)。

这不是每一个字符串,而是你代码中的常量字符串表达式。例如:
string s = "abc" + "def";

只有一个字符串表达式 - IL 将是 "abcdef" 的 ldstr(编译器可以计算组合的表达式)。

这不会影响性能。

在运行时生成的字符串不会自动进行内部化,例如:

int i = GetValue();
string s = "abc" + i;

这里,“abc”是内部化的,但“abc8”不是。另外请注意:

char[] chars = {'a','b','c'};
string s = new string(chars);
string t = "abc";

请注意,st是不同的引用(分配给t的文本已内部化,但分配给s的新字符串则没有)。


1
你的意思不是t被interned了,而是新字符串s没有被interned吗? - Kevin
@Kevin 谢谢,已澄清。 - Marc Gravell

3

.NET是否对我使用的每个字符串都使用字符串池?

不是,但它会对那些在编译时就已知的字符串使用字符串池,因为它们在代码中是常量。

string x = "abc"; //interned
string y = "ab" + "c"; //interned as the same string because the
                       //compiler can work out that it's the same as
                       //y = "abc" at compile time so there's no need
                       //to do that concatenation at run-time. There's
                       //also no need for "ab" or "c" to exist in your
                       //compiled application at all.
string z = new StreamReader(new FileStream(@"C:\myfile.text")).ReadToEnd();
                       //z isn't interned because it isn't known at compile
                       //time. Note that @"C:\myfile.text" is interned because
                       //while we don't have a variable we can access it by
                       //it is a string in the code.

如果这样做,难道不会影响性能吗?
不会,它会提高性能:
第一:所有这些字符串都将在应用程序的某个地方存在于内存中。如果使用字符串驻留(interning),我们就不需要不必要的复制,因此使用的内存更少。
第二:它使我们知道只有来自字符串池中驻留的字符串才能进行超快速的字符串比较。
第三:尽管这并不经常发生,但是它提高了其他比较的性能。考虑一下存在于内置比较器之一中的代码:
public override int Compare(string x, string y)
{
    if (object.ReferenceEquals(x, y))
    {
        return 0;
    }
    if (x == null)
    {
        return -1;
    }
    if (y == null)
    {
        return 1;
    }
    return this._compareInfo.Compare(x, y, this._ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
}

这是针对排序的,但同样适用于等式/不等式检查。检查两个字符串是否相等或将它们放入顺序需要进行O(n)操作,其中n与字符串的长度成比例(即使在某些跳过和巧妙处理的情况下,它仍然成比例)。这对于长字符串来说可能很慢,比较字符串也是许多应用程序经常做的事情 - 这是提速的好地方。对于相等情况来说最慢(因为在我们找到差异之后立即返回值,但必须完全检查相等的字符串)。
任何东西总是等于自己,即使您重新定义了“等于”的含义(大小写敏感、不敏感、不同的文化 - 一切都等于自己,如果您创建了一个不遵循这一点的Equals()重载,则会出现错误)。每件事总是与相同点有序。这意味着两件事:
1. 我们可以始终认为某物等于它本身,而无需进行任何其他工作。 2. 我们可以始终为将某物与其本身进行比较的比较值提供0,而无需进行任何其他工作。
因此,上面的代码在不必进行更复杂和昂贵的比较的情况下快捷地处理此情况。另外,没有副作用,因为如果我们没有涵盖此案例,我们将不得不添加一个测试,以在值都传递null的情况下进行比较。
现在,自然而然地,将某个东西与自身进行比较经常会出现在某些算法工作的方式中,因此这总是值得做的。但是,字符串内部化增加了我们拥有的两个不同字符串(例如,在您问题开始时的x和z)实际上相同的时间,因此它增加了我们使用短路的频率。
大多数情况下,这只是微小的优化,但我们可以免费获得它,并且我们如此经常拥有它,因此拥有它非常好。从这一点来看,实际应用是 - 如果您正在编写Equals或Compare,请考虑是否也应该使用此快捷方法。
然后,相关问题是“我应该内部化所有内容吗?”
在这里,我们必须考虑已编译的字符串没有的缺点。内部化对于已编译的字符串永远不会浪费,因为它们必须存在某处。但是,如果您从文件中读取字符串,将其内部化,然后再也不使用它,那么它将生存很长时间,这是浪费的。但是,如果您经常读取包含某些标识符的一堆项目。您经常使用这些标识符将项目与另一个源中的数据匹配。只有一小组标识符将看到(例如,只有几百个可能的值)。然后,由于相等性检查就是所有这些字符串所涉及的内容,并且没有太多的它们,内部化(在读取的数据和与其进行比较的数据上 - 否则毫无意义)变得更好。
或者,假设有成千上万个这样的对象,并且我们匹配它的数据总是被缓存在内存中 - 这意味着这些字符串总是会在内存中某个地方,所以进行合并变成了一个显而易见的胜利策略。(除非存在大量“未找到”结果 - 对这些标识符进行合并以便找不到匹配结果是失败的)。
最后,同样的基本技术可以有不同的实现方式。例如,XmlReader将它正在比较的字符串存储在一个NameTable中,其作用类似于私有合并池,但整个过程可以在完成时进行收集。您还可以将该技术应用于任何在池化期间不会被更改的引用类型(保证的最佳方法是使其不可变,因此它在任何时间都不会更改)。对于带有大量重复内容的大型集合,使用这种技术可以大幅度减少内存使用量(我最大的节省至少达到了16GB - 可能会更多,但在应用该技术之前服务器在那一点附近就会崩溃),或加快比较速度。

1

字符串字面量会自动进行内部化。

程序创建的字符串默认情况下不会被内部化(用户输入的字符串也不会)。

在上述代码中,“Some Text”和“Some Other Text”都已经被内部化,因此在使用这些字面量时,你会看到引用的是内部化版本。

如果你的代码中有:

string.Format("{0} {1}", "Some", "Text")

您会发现返回的引用与其他文字不同。


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接