为什么.NET IL总是创建新的字符串对象,即使更高级别的代码引用了现有的字符串对象



背景:我们有一个包含数千个伪代码函数的XML文档。我已经编写了一个实用程序来解析这个文档并从中生成C#代码

public class SomeClass
{
    public string Func1() { return "Some Value"; }
    public string Func2() { return "Some Other Value"; }
    public string Func3() { return "Some Value"; }
    public string Func4() { return "Some Other Value"; }
    // ...
}

重要的一点是,每个字符串值都可能由多个方法返回。我假设,通过进行一个小的更改,使方法返回对静态成员字符串的引用,这将减少程序集的大小,并减少程序的内存占用。例如:

public class SomeClass
{
    private const string _SOME_VALUE = "Some Value";
    private const string _SOME_OTHER_VALUE = "Some Other Value";
    // ...
    public string Func1() { return _SOME_VALUE; }
    public string Func2() { return _SOME_OTHER_VALUE; }
    public string Func3() { return _SOME_VALUE; }
    public string Func4() { return _SOME_OTHER_VALUE; }
    // ...
}

但令我惊讶的是,使用.NETildasm.exe实用程序进行的检查显示,在这两种情况下,函数的IL是相同的。这是给其中一个人的。无论哪种方式,硬编码值都会与ldstr:一起使用

.method public hidebysig instance string
        Func1() cil managed
{
  // Code size       6 (0x6)
  .maxstack  8
  IL_0000:  ldstr      "Some Value"
  IL_0005:  ret
} // end of method SomeClass::Func1

事实上,"优化"版本稍差一些,因为它在程序集中包含静态字符串成员。当我使用string之外的其他对象类型重复这个实验时,我看到了预期的差异。请注意,程序集是在启用优化的情况下生成的。

问题:为什么.NET显然总是创建一个新的字符串对象,而不管代码是否引用了现有的字符串对象?

  IL_0000:  ldstr      "Some Value"
  IL_0005:  ret

反汇编程序太有用了,无法向您显示实际情况。您可以从IL地址中判断,请注意ldstr指令只占用5个字节。太少了,无法存储字符串。使用View+Show标记值可以查看它的真实外观。您现在还将看到相同的字符串使用相同的令牌值。这被称为"实习"。

在程序被jit之后,令牌值仍然不会显示字符串的实际存储位置。字符串文字进入"loader堆",这是一个与垃圾收集堆不同的堆。它是存储静态项的堆。或者换一种说法:字符串文字是高度优化的,而且非常便宜。你自己做得再好不过了。

请参阅http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldstr(v=vs.71(.aspx

公共语言基础结构(CLI(保证引用具有相同的字符序列返回完全相同的字符串对象(a被称为"字符串插入"的过程(。

我现在没有Visual Studio,所以我无法给出我想要的简洁答案。您显示的MSIL使其看起来好像字符串没有被插入。尝试使用object.ReferenceEquals(...)查看是否真的是这样,甚至在文本编辑器中打开编译后的库。如果字符串没有被实习,可能会有一个项目设置来启用实习(同样,我面前没有VS来给你一个确切的参考(。

您的另一个选择是将字符串定义更改为static readonly,这将使方法返回对静态实例的引用。请注意,使用此方法将创建一个隐式静态构造函数,该构造函数将在首次引用类时创建字符串实例。

IL代码中的字符串总是,因此不会构造新的字符串。您可以使用以下代码对此进行验证:

     string str = "123";
     string isinterned = string.IsInterned (str);
     Console.WriteLine(ReferenceEquals(str, isinterned));

常量旨在用作任何地方(在IL中(的文字,而不仅仅是字符串。如果这不是您想要的(我知道一些有效的情况,比如为程序集的新版本获取更新的"常数值"(,请尝试这样的static readonly

public static readonly string _SOME_VALUE = "Some Value";
public static readonly string _SOME_OTHER_VALUE = "Some Other Value";

.NET将String对象模拟为基元类型,尽管它是Char数组。主类型在传递给函数时总是被克隆。因此,在执行任何操作或传递时,.NET将始终克隆String值。

最新更新