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

Pra*_*eek 36 c# generics comparison boxing equality

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

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

csc.exe 生成的IL是:

ldarg.1
box        !T
ldarg.2
box        !T
ceq
Run Code Online (Sandbox Code Playgroud)

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

但是,如果约束表明"T"永远不应该是值类型,为什么编译器会尝试框r1r2

Meh*_*ari 43

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

通用语言基础设施规范(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
Run Code Online (Sandbox Code Playgroud)

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

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

void Method() {
    while (true) {
    }
    DoSomething();  // unreachable code
}
Run Code Online (Sandbox Code Playgroud)

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

bool Condition() {
   return true;
}

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

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

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

  • @Ani:基本上.JIT将负责将其删除. (3认同)

Eri*_*ert 16

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)
    {
Run Code Online (Sandbox Code Playgroud)

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

  • 感谢内部的这些信息. (2认同)