说明引用字符串的 IL 生成代码

Clarification of IL generated code referenced to strings

我今天正在执行一些重构,我注意到一件我无法理解的奇怪事情......或者更好的是部分同意我在网上找到的内容,但仍有一些问题。

请考虑这个简单的例子

 class Program
{
    public static readonly string a = "a";
    public const string b = "b";
    static void Main(string[] args)
    {

        Console.WriteLine(a);

        Console.WriteLine(b);
    }
}

现在,如果我查看生成的 IL 代码(通过 IL 浏览器从 resharp 获取)

我看到下面的代码

.method private hidebysig static void 
Main(
  string[] args
) cil managed 
{
 .entrypoint
.maxstack 8

// [16 13 - 16 34]
IL_0000: ldsfld       string ConsoleApp4.Program::a
IL_0005: call         void [mscorlib]System.Console::WriteLine(string)

// [18 13 - 18 34]
IL_000a: ldstr        "b"
IL_000f: call         void [mscorlib]System.Console::WriteLine(string)

// [19 9 - 19 10]
IL_0014: ret          

 } // end of method Program::Main

 .method public hidebysig specialname rtspecialname instance void 
.ctor() cil managed 
{
 .maxstack 8

IL_0000: ldarg.0      // this
IL_0001: call         instance void [mscorlib]System.Object::.ctor()
IL_0006: ret          

 } // end of method Program::.ctor

 .method private hidebysig static specialname rtspecialname void 
.cctor() cil managed 
 {
.maxstack 8

// [11 9 - 11 47]
IL_0000: ldstr        "a"
IL_0005: stsfld       string ConsoleApp4.Program::a
IL_000a: ret          

 } // end of method Program::.cctor
  } // end of class ConsoleApp4.Program

静态字符串的行为与我预期的一样。 而不是 const 它在堆栈上加载一个新值......事实上看着 ldstr 操作码 here 它说

Pushes a new object reference to a string literal stored in the metadata

我读过 here

Now, wherever myInt is referenced in the code, instead of having to do a "ldloc.0" to get the value from the variable, the MSIL just loads the constant value which is hardcoded into the MSIL. As such, there's usually a small performance and memory advantage to using constants.However, in order to use them you must have the value of the variable at compile time, and any references to this constant at compile time, even if they're in a different assembly, will have this substitution made.

如果您在编译时知道常量的值,那么常量肯定是一个有用的工具。如果你不这样做,但又想确保你的变量只被设置一次,你可以在C#中使用readonly关键字(它映射到MSIL中的initonly)来表明变量的值只能在构造函数中设置;在那之后,更改它是错误的。这通常在字段帮助确定 class 的身份时使用,并且通常设置为等于构造函数参数。

但我为什么要体验更好的性能呢? (即使考虑到它很容易理解)?内存占用情况如何?

提前致谢

考虑这段代码:

public class Program
{
    public const int ConstField1 = 1;
    public const int ConstField2 = 2;
    public const int ConstField3 = 3;
    public const int ConstField4 = 4;
}

这4个const int32数只存储在程序集元数据对应的内存中(因此可以通过反射获取),实际运行时的类型信息中并没有。与 static readonly 相比,这节省了 16 个字节的内存。在字符串的情况下,运行time 也不必在字符串实际用于其他代码之前分配字符串(因为 ldstr 不用于初始化字段)。您可能会争辩说这不会节省太多,但请考虑枚举 - 它们基本上是具有大量常量字段的静态类型。

性能提升也很明显——因为不需要在每次使用时都获取值,减少了内存碎片,并且可以对其他情况下不可能的值执行其他优化(例如作为简化表达式,如 BindingFlags.NonPublic | BindingFlags.Instance)。此外,不需要调用静态构造函数,这是另一点(尽管在某些情况下可能不会调用,请参见beforefieldinit)。