2012-09-16 29 views
8

有局部变量在基于堆栈的中间语言,如CIL或Java字节码,为什么会有局部变量?只能使用堆栈。手工制作的IL可能不那么容易,但编译器肯定可以做到。但我的C#编译器没有。为什么是基于堆栈的字节码IL

堆栈和局部变量都是方法专用的,并在方法返回时超出范围。所以它不可能与方法外部可见的副作用(来自另一个线程)有关。

JIT编译器将消除加载和存储都栈槽和生成机器码的时候,如果我是正确的局部变量,所以JIT编译器也没有看到局部变量的需要。

在另一方面,C#编译器生成的载入和存储局部变量,启用优化编译时也是如此。为什么?


采取例如,下面的人为的例子的代码:

static int X() 
{ 
    int a = 3; 
    int b = 5; 
    int c = a + b; 
    int d; 
    if (c > 5) 
     d = 13; 
    else 
     d = 14; 
    c += d; 
    return c; 
} 

当在C#编译,以优化时,其产生:

ldc.i4.3  # Load constant int 3 
    stloc.0   # Store in local var 0 
    ldc.i4.5  # Load constant int 5 
    stloc.1   # Store in local var 1 
    ldloc.0   # Load from local var 0 
    ldloc.1   # Load from local var 1 
    add    # Add 
    stloc.2   # Store in local var 2 
    ldloc.2   # Load from local var 2 
    ldc.i4.5  # Load constant int 5 
    ble.s label1 # If less than, goto label1 
    ldc.i4.s 13  # Load constant int 13 
    stloc.3   # Store in local var 3 
    br.s label2  # Goto label2 
label1: 
    ldc.i4.s 14  # Load constant int 14 
    stloc.3   # Store in local var 3 
label2: 
    ldloc.2   # Load from local var 2 
    ldloc.3   # Load from local var 3 
    add    # Add 
    stloc.2   # Store in local var 2 
    ldloc.2   # Load from local var 2 
    ret    # Return the value 

注意加载和存储的四个局部变量。我可以在不使用任何局部变量的情况下编写完全相同的操作(不考虑明显的常量传播优化)。

ldc.i4.3  # Load constant int 3 
    ldc.i4.5  # Load constant int 5 
    add    # Add 
    dup    # Duplicate top stack element 
    ldc.i4.5  # Load constant int 5 
    ble.s label1 # If less than, goto label1 
    ldc.i4.s 13  # Load constant int 13 
    br.s label2  # Goto label2 
label1: 
    ldc.i4.s 14  # Load constant int 14 
label2: 
    add    # Add 
    ret    # Return the value 

这对我来说似乎是正确的,而且更短,更高效。那么,为什么基于堆栈的中间语言有局部变量呢?为什么优化编译器如此广泛地使用它们?

+3

您不能*总是*做出如您在示例中演示的简单转换。 –

+0

这个问题问为什么“命名的插槽”是*需要*或为什么C#“优化”的输出看起来过于冗长(例如*在这种情况下使用*)? – 2012-09-17 00:02:06

+0

即使在某些情况下(这种情况?)需要或使用“命名空位”,为什么优化编译器不能消除大多数加载和存储?这似乎很微不足道。我肯定错过了什么。 – Virtlink

回答

4

根据不同的情况,特别是当呼叫参与其中的参数进行重新排序相匹配的呼叫,一个纯粹的堆栈是不够的,如果你没有在您的处置寄存器或变量。如果你只想做这个栈,你需要额外的栈操作abilties,比如交换/交换堆栈的两个顶层项目的能力。最后,虽然在这种情况下可能会将所有东西都表示为纯粹的基于堆栈的形式,但它可能会给代码增加很多复杂性,使其膨胀并使其更难以优化(局部变量是理想的候选人被缓存在寄存器中)。

还记得在.NET中,你可以通过引用传递参数,你怎么可以创建IL此方法调用没有本地变量?

bool TryGet(int key, out string value) {} 
+0

我不明白签名如何改变问题本身。对于IL来说,可能有一个“writeToOutParam”操作(副作用),否则完全是基于堆栈的。 – 2012-09-17 00:06:49

+1

我认为Lucero是指呼叫方的来电方。当没有局部变量时,局部变量的地址作为第二个参数传递给'TryGet'方法?但是,这当然可以成为堆栈插槽的“指针”。而且,堆栈中的值不是更好的候选缓存在寄存器中吗? – Virtlink

0

这个答案纯粹是推测 - 但我怀疑答案有3个部分。

1:代码转变为喜欢在Dup的局部变量是非常不平凡的,甚至当你忽视的副作用。它为优化增加了很多复杂性和潜在的大量执行时间。

2:你不能忽视副作用。在一切都只是一个文字的例子中,很容易知道这些值是堆栈还是当地人,因此完全可以控制当前的指令。一旦这些值来自堆,静态内存或方法调用,就不能再使用Dup来替换当地的东西了。更改顺序可能会改变事情的实际工作方式,并可能由于副作用或外部访问共享内存而导致意想不到的后果。这意味着通常你不能进行这些优化。

3:堆栈值比局部变量不是一个好假设更快的假设 - 对于特定的IL->机器代码转换,堆栈值更快,但是没有理由为什么智能JIT不会将堆栈位置放入内存中,而将局部变量放入寄存器中。 JIT的工作是了解当前机器的速度和速度,以及解决问题的时间,这是JIT的工作。通过设计,CIL编译器无法回答当地人或堆栈是否更快;所以这些结果之间的可测量差异仅在代码大小上。

放在一起,1意味着它很难并且具有不平凡的成本,2意味着现实世界中有价值的情况很少,而3意味着1和2无关。

即使目标是最小化CIL大小,这是CIL编译器的可测量目标,理由#2将此描述为对少量情况的小改进。帕累托原理不能告诉我们实现这种优化是一个糟糕的主意,但它会建议有可能更好地使用开发人员时间。

+0

以及我忘记的东西 - 在编译速度非常重要的真正JIT中,避免Dup操作可能导致最小堆栈大小,从而使JIT的生活更轻松。给出相同的结果,最好比结果更快达到结果。 – danwyand

+0

但我仍然看到编译器放入什么似乎完全不必要的局部变量(例如'ldfld x; stloc.1; ldarg.0; ldloc.1; ldc.i4.1; add; stfld x;' - 什么目的'loc.1'服务?),当他们什么都不加。或者使用'ldarg.0; ldarg.0;'当'ldarg.0;在所有情况下似乎都是安全的? – NetMage