IL& .net中的堆栈实现?

时间:2012-12-08 11:41:38

标签: c# .net compiler-construction il

我写了一个简单的程序来检查IL是如何工作的:

void Main()
{

 int a=5;
 int b=6;
 if (a<b) Console.Write("333");
 Console.ReadLine();
}

IL:

IL_0000:  ldc.i4.5    
IL_0001:  stloc.0     
IL_0002:  ldc.i4.6    
IL_0003:  stloc.1     
IL_0004:  ldloc.0     
IL_0005:  ldloc.1     
IL_0006:  bge.s       IL_0012
IL_0008:  ldstr       "333"
IL_000D:  call        System.Console.Write
IL_0012:  call        System.Console.ReadLine

我正在努力了解已实施的效率:

  • 在#1行(IL代码),它将值5推送到堆栈上(4个字节,即int32)

  • 在第2行(IL代码),它从堆栈POP到局部变量。

接下来的两行也是如此。

然后,它将这些局部变量加载到堆栈中,然后 THEN 评估bge.s

问题#1

他为什么要将局部变量加载到堆栈中?值已经在堆栈中。但是为了把它们放在局部变量中,他将它们加以限制。这不是浪费吗?

我的意思是,为什么代码不能像:

IL_0000:  ldc.i4.5
IL_0001:  ldc.i4.6    
IL_0002:  bge.s       IL_0004
IL_0003:  ldstr       "333"
IL_0004:  call        System.Console.Write
IL_0005:  call        System.Console.ReadLine

我的代码示例只有5行代码。那50,000,000行代码呢? IL会发出大量额外的代码

问题#2

查看代码地址:

enter image description here

  • IL_0009地址在哪里?它不应该是连续的吗?

P.S。我在+发布模式下使用Optimize标志

4 个答案:

答案 0 :(得分:10)

我可以轻松回答第二个问题。说明书是可变长度的。例如,ldstr "333"ldstr(在地址8)的操作码组成,后跟表示字符串的数据(对用户字符串表中字符串的引用)。

与之后的call语句类似 - 您需要call操作码本身以及要调用的函数的信息。

将诸如4或6之类的小值推送到堆栈上的指令没有额外数据的原因是因为这些值被编码到操作码本身中。

有关说明和编码,请参阅here

关于第一个问题,您可能需要查看this blog entry by Eric Lippert, one of the C# developers,其中说明:

  

/ optimize标志不会改变我们的大量发射和生成逻辑。我们尝试始终生成简单,可验证的代码,然后依靠抖动在生成真实机器代码时进行大量优化。

答案 1 :(得分:7)

在这个级别上有关IL效率的推理是没有意义的。

JIT将完全消除堆栈,将所有堆栈操作转换为中间三地址代码(并进一步转换为SSA)。由于IL 从不解释,因此堆栈操作不应该高效且优化。

例如,请参阅开源Mono实现。

答案 2 :(得分:6)

  

他为什么要将局部变量加载到堆栈中?这些值已经在堆栈中。但他为了将它们置于局部变量而加入了它们。这不是浪费吗?

浪费了什么?您必须记住IL(通常)不会按原样执行,它由JIT编译器再次编译,JIT编译器执行大多数优化。使用“中间语言”的一个要点是可以在一个地方实现优化:JIT编译器和每种语言(C#,VB.NET,F#,...)不必再重复实现它们。 Eric Lippert在他的文章Why IL?

中解释了这一点
  

IL_0009地址在哪里?它不应该是连续的吗?

让我们看一下ldstr指令的规范(来自ECMA-335):

  

III.4.16 ldstr - 加载文字字符串

     

格式:72&lt; T&gt; [...]

     

ldstr指令将表示存储在元数据中的文字的新字符串对象推送为字符串(字符串文字)。

对上述元数据的引用和&lt; T&gt;表示指令的字节72后跟元数据标记,该标识符指向包含字符串的表。这样的象征有多大?从同一文件的第III.1.9节:

  

许多CIL指令后面跟着“元数据标记”。这是一个4字节的值,用于指定元数据表[...]

中的行

因此,在您的情况下,指令的字节72位于地址0008处,并且令牌(在这种情况下为0x70000001,其中0x70字节表示用户字符串表)位于地址0009至000C。

答案 3 :(得分:0)

对所有关于“额外代码”的讨论给出最终答案。

C#编译器读取int a=5;并将其转换为:

ldc.i4.5
stloc.0

然后它转到下一行并读取int b=6;,并转换为:

ldc.i4.6
stloc.1

然后用if语句读取下一行,依此类推。

当从C#编译为IL时,它逐行读取并将该行转换为IL,而不是在查看其他行时将其转换为该行。

要在此阶段优化IL并删除“额外代码”(您称之为“额外代码”),C#编译器必须检查所有IL代码,构建它的树表示,删除所有不需要的节点,然后编写它再次作为IL。这不是C#编译器应该做的事情,因为当从IL到机器语言时,JIT编译器将完成此操作。

因此,您认为额外的代码不是额外代码,它是C#编译器从C#代码中读取的语句的一部分,并且在JIT编译器将代码编译为本机可执行文件时将被删除。

这是对C#代码如何翻译的高级解释,因为我认为你没有在编译器构造中使用任何类或类似的东西。 如果您想了解更多信息,请访问互联网上的书籍和页面。