为什么基于堆栈的 IL 字节码中存在局部变量

2022-09-04 22:11:39

在基于堆栈的中间语言(如 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

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


答案 1

根据具体情况,特别是当涉及调用时,必须对参数进行重新排序以匹配调用,如果没有寄存器或变量可供使用,则纯堆栈是不够的。如果要使此堆栈仅化,则需要其他堆栈操作功能,例如交换/交换堆栈的两个顶部项的功能。

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

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

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

答案 2

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

1:代码转换为首选 Dup 而不是局部变量是非常不平凡的,即使你忽略了副作用。它增加了很多复杂性,并可能为优化增加大量执行时间。

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

3:假设堆栈值比局部变量更快,这不是一个好的假设 - 对于特定的IL->机器代码转换,堆栈值更快可能是正确的,但是没有理由为什么智能JIT不会将堆栈位置放入内存中并将局部变量放入寄存器中。JIT的工作是知道当前机器的快速和慢,JIT的工作是解决问题。根据设计,CIL编译器没有关于本地或堆栈是否更快的答案;因此,这些结果之间的可测量差异仅在于代码大小。

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

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


推荐