C# 对引用字符串的IL生成代码的澄清

C# 对引用字符串的IL生成代码的澄清,c#,.net,cil,C#,.net,Cil,我今天正在进行一些重构,我注意到一件奇怪的事情,我不明白……或者更好的是,我部分同意我在网上发现的东西,但仍然有一些问题 请考虑这个简单的例子 class Program { public static readonly string a = "a"; public const string b = "b"; static void Main(string[] args) { Console.WriteLine(a); Con

我今天正在进行一些重构,我注意到一件奇怪的事情,我不明白……或者更好的是,我部分同意我在网上发现的东西,但仍然有一些问题

请考虑这个简单的例子

 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代码(通过resharp的IL浏览器获得)

我看到了下面的代码

.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
对于静态字符串,它的行为与我预期的一样。
相反,对于常量,它在堆栈上加载一个新值。。。事实上,看看它说的ldstr操作码

将新对象引用推送到元数据中存储的字符串文字

我读过

现在,只要代码中引用myInt,MSIL就只加载硬编码到MSIL中的常量值,而不必执行“ldloc.0”从变量中获取值。因此,使用常量通常会带来较小的性能和内存优势。但是,为了使用常量,您必须在编译时拥有变量的值,并且在编译时对该常量的任何引用,即使它们位于不同的程序集中,都将进行此替换

如果您在编译时知道该值,常数当然是一个有用的工具。如果不需要,但希望确保变量只设置一次,可以使用C#中的readonly关键字(在MSIL中映射到initonly)来指示变量的值只能在构造函数中设置;在那之后,更改它是一个错误。当一个字段有助于确定类的标识时,通常使用该字段,并且该字段通常被设置为等于构造函数参数

但是为什么我要体验更好的表现呢?(即使考虑到这相当容易旅行)?内存占用情况如何

提前感谢

请考虑以下代码:

public class Program
{
    public const int ConstField1 = 1;
    public const int ConstField2 = 2;
    public const int ConstField3 = 3;
    public const int ConstField4 = 4;
}
四个const int32数字只存储在与程序集元数据相对应的内存中(因此可以通过反射获得),而不存储在实际的运行时类型信息中。与静态只读相比,这节省了16字节的内存。对于字符串,在其他代码中实际使用字符串之前,运行时也不必分配字符串(因为
ldstr
不用于初始化字段)。你可能会认为这并不能节省很多,但是考虑枚举——它们基本上是静态类型,有很多const字段。
性能的提高也是显而易见的——因为不需要每次使用该值时都获取它,内存碎片会减少,并且可以对该值执行其他用户无法执行的其他优化(例如简化表达式,如
bindingsflags.NonPublic | bindingsflags.Instance
)。另外,不需要调用静态构造函数,因此这是另一点(尽管在某些情况下可能不会调用它,请参见
beforefieldinit
)。

“相反,对于常量,它会在堆栈上加载一个新值”-在这两种情况下,它不是都会在堆栈上加载引用吗?我不清楚担心的是什么。(如果你担心的话,它不是每次都创建一个新的字符串对象。)“静态字符串”没有帮助
a
是存储对字符串对象的引用的变量<代码>b不是变量,而是常量。编译器只是在代码中使用常量标识符的地方替换文本
ldstr
经过了大量优化,因为它非常常见,抖动会生成一条MOV reg、imm指令
ldsfld
也不是无精打采的,但需要内存访问MOV reg[addr]。这并不重要,因为实际使用字符串通常需要更长的时间。当您使用Console.WriteLine()时,时间要长得多。谢谢您的回复,因为@HansPassant我对Console.WriteLine()的使用只是一个示例。。。