13

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

堆栈和局部变量都是方法的私有变量,并且在方法返回时超出范围。因此,它与从方法外部(来自另一个线程)可见的副作用没有任何关系。

如果我是正确的,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

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

4

2 回答 2

7

视情况而定,但尤其是在涉及必须重新排序参数以匹配调用的调用时,如果您没有可用的寄存器或变量,则纯堆栈是不够的。如果你想让这个只有堆栈,你需要额外的堆栈操作能力,例如交换/交换堆栈顶部的两个项目的能力。

最后,虽然在这种情况下可以将所有内容表达为纯基于堆栈的,但它会给代码增加大量复杂性,使代码膨胀并使其更难以优化(局部变量是理想的候选者)缓存在寄存器中)。

还要记住,在 .NET 中可以通过引用传递参数,如何在没有局部变量的情况下为该方法调用创建 IL?

bool TryGet(int key, out string value) {}
于 2012-09-16T23:48:59.290 回答
3

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

1:代码转换为更喜欢 Dup 而不是局部变量是非常重要的,即使您忽略了副作用。它为优化增加了很多复杂性和潜在的大量执行时间。

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

3:堆栈值比局部变量更快的假设不是一个好的假设——对于特定的 IL-> 机器代码转换,堆栈值更快,但没有理由不让智能 JIT将堆栈位置放入内存,将局部变量放入寄存器。知道当前机器什么是快什么是慢是 JIT 的工作,解决问题是 JIT 的工作。按照设计,CIL 编译器无法回答是本地还是堆栈更快。因此,这些结果之间的可测量差异仅在于代码大小。

总而言之,1 意味着它很难并且具有不小的成本,2 意味着它有价值的现实世界案例很少,3 意味着 1 和 2 无论如何都是无关紧要的。

即使目标是最小化 CIL 大小,这对于 CIL 编译器来说是一个可衡量的目标,原因 #2 将其描述为对少量案例的小改进。帕累托原则不能告诉我们实现这种优化是一个坏主意,但它会建议可能更好地利用开发人员时间。

于 2013-12-17T22:14:24.077 回答