为什么编译器会发出框指令来比较引用类型的实例?

时间:2010-12-17 23:11:33

标签: c# generics comparison equality boxing

这是一个简单的泛型类型,其唯一的泛型参数约束为引用类型:

class A<T> where T : class
{
    public bool F(T r1, T r2)
    {
        return r1 == r2;
    }
}

csc.exe生成的 IL 是:

ldarg.1
box        !T
ldarg.2
box        !T
ceq

因此,在进行比较之前,每个参数都是装箱

但是如果约束表明“T”永远不应该是值类型,那么为什么编译器会尝试将r1r2 打包?

2 个答案:

答案 0 :(得分:43)

需要满足生成的IL的可验证性约束。请注意,无法验证的并不一定意味着不正确。只要其安全上下文允许运行无法验证的代码,它就可以在没有box指令的情况下正常工作。验证是保守的,基于固定的规则集(如可达性)。为简化起见,他们选择不关心验证算法中是否存在泛型类型约束。

  

Common Language Infrastructure Specification (ECMA-335)

     

第9.11节:通用参数的约束

     

...   泛型参数的约束仅限制泛型参数的类型   可以用。实例化。   验证(参见第III部分)要求使用字段,属性或方法   已知通用参数通过满足约束提供,但不能   通过通用参数直接访问/调用,除非它是第一次装箱   (参见第III部分)或callvirt指令以constrained前缀指令为前缀。 ...

删除box说明会导致无法验证的代码:

.method public hidebysig instance bool 
       F(!T r1,
         !T r2) cil managed
{
   ldarg.1
   ldarg.2
   ceq
   ret
}


c:\Users\Mehrdad\Scratch>peverify sc.dll

Microsoft (R) .NET Framework PE Verifier.  Version  4.0.30319.1
Copyright (c) Microsoft Corporation.  All rights reserved.

[IL]: Error: [c:\Users\Mehrdad\Scratch\sc.dll : A`1[T]::F][offset 0x00000002][fo
und (unboxed) 'T'] Non-compatible types on the stack.
1 Error(s) Verifying sc.dll

更新(对评论的回答):正如我上面提到的,可验证性并不等同于正确性(这里我从类型安全的角度谈论“正确性”)。 可验证的程序是正确的程序的严格子集(即所有可验证的程序都是明确正确的,但是有正确的程序是不可验证的)。因此,可验证性是一种比正确性更强的属性。由于C#是一种图灵完备语言,Rice's theorem表明在一般情况下证明程序正确是不可判定的。

让我们回到我的可达性类比,因为它更容易解释。假设您正在设计C#。有一点想到的是什么时候发出关于无法访问的代码的警告,并在优化器中完全删除那段代码,但是你将如何检测所有无法访问的代码?赖斯的定理再一次说你不能为所有程序做到这一点。例如:

void Method() {
    while (true) {
    }
    DoSomething();  // unreachable code
}

这是C#编译器实际警告的内容。但它没有警告:

bool Condition() {
   return true;
}

void Method() {
   while (Condition()) {
   }
   DoSomething();  // no longer considered unreachable by the C# compiler
}

人类可以证明在后一种情况下控制流量永远不会到达那条线。有人可能会争辩说,在这种情况下,编译器可以静态证明DoSomething无法访问,但事实并非如此。为什么?关键是你不能为所有程序做到这一点,所以你应该在某个点画线。在此阶段,您必须定义可判定的属性并将其命名为“reachability”。例如,为了实现可达性,C#坚持使用常量表达式,并且根本不会查看函数的内容。分析的简单性和设计的一致性是决定在哪里划线的重要目标。

回到我们的可验证性概念,这是一个类似的问题。与正确性不同,可验证性是一种可判定的属性。作为运行时设计器,您必须根据性能考虑因素,易于实现,易于规范,一致性来决定如何定义可验证性,从而使编译器可以轻松地生成可验证的代码。像大多数设计决策一样,它涉及很多权衡。最终,CLI设计人员决定在检查可验证性时,他们根本不会考虑通用约束。

答案 1 :(得分:15)

Mehrdad的回答非常好;我只想补充几点:

首先,是的,在这种情况下,这只是为了让验证者高兴。抖动当然应该优化掉装箱指令,因为装箱参考类型没有意义。

然而,有些情况下保持验证者的快乐,我们必须引入未经优化的拳击指令。例如,如果你说:

class B<T> { public virtual void M<U>(U u) where U : T {...} }
class D : B<int> 
{ 
    public override void M<U>(U u)
    {

C#编译器知道在D.M中,U只能是int。然而,为了可以验证,有些情况下你必须装箱对象,然后取消装箱到int。抖动并不总能优化掉这些;我们已经向抖动团队指出,这是一种可能的优化,但情况是如此模糊,以至于不太可能为许多实际客户带来巨大的胜利。他们可以花时间进行更大胆的优化。