为什么针对通用类型会发出“box”指令?

12

这里有一个相当简单的通用类。泛型参数被限制为引用类型。IRepositoryDbSet也包含相同的约束。

public class Repository<TEntity> : IRepository<TEntity>
    where TEntity : class, IEntity
{
    protected readonly DbSet<TEntity> _dbSet;
    public void Insert(TEntity entity)
    {
        if (entity == null) 
        throw new ArgumentNullException("entity", "Cannot add null entity.");
        _dbSet.Add(entity);
    }
}

编译后的中间语言包含box指令。这是发布版本(尽管调试版本也包含它)。

.method public hidebysig newslot virtual final 
    instance void  Insert(!TEntity entity) cil managed
{
  // Code size       38 (0x26)
  .maxstack  8
  IL_0000:  ldarg.1
  >>>IL_0001:  box        !TEntity
  IL_0006:  brtrue.s   IL_0018
  IL_0008:  ldstr      "entity"
  IL_000d:  ldstr      "Cannot add null entity."
  IL_0012:  newobj     instance void [mscorlib]System.ArgumentNullException::.ctor(string,
                                           string)
  IL_0017:  throw
  IL_0018:  ldarg.0
  IL_0019:  ldfld      class [EntityFramework]System.Data.Entity.DbSet`1<!0> class Repository`1<!TEntity>::_dbSet
  IL_001e:  ldarg.1
  IL_001f:  callvirt   instance !0 class [EntityFramework]System.Data.Entity.DbSet`1<!TEntity>::Add(!0)
  IL_0024:  pop
  IL_0025:  ret
} // end of method Repository`1::Insert

更新:

使用object.Equals(entity, default(TEntity))甚至更糟:

  .maxstack  2
  .locals init ([0] !TEntity CS$0$0000)
  IL_0000:  ldarg.1
  >>>IL_0001:  box        !TEntity
  IL_0006:  ldloca.s   CS$0$0000
  IL_0008:  initobj    !TEntity
  IL_000e:  ldloc.0
  >>>IL_000f:  box        !TEntity
  IL_0014:  call       bool [mscorlib]System.Object::Equals(object,
                                object)
  IL_0019:  brfalse.s  IL_002b

更新2:

对于那些感兴趣的人,这里是jit编译后在调试器中显示的代码:

0cd5af28 55              push    ebp
0cd5af29 8bec            mov     ebp,esp
0cd5af2b 83ec18          sub     esp,18h
0cd5af2e 33c0            xor     eax,eax
0cd5af30 8945f0          mov     dword ptr [ebp-10h],eax
0cd5af33 8945ec          mov     dword ptr [ebp-14h],eax
0cd5af36 8945e8          mov     dword ptr [ebp-18h],eax
0cd5af39 894df8          mov     dword ptr [ebp-8],ecx
    //entity reference to [ebp-0Ch]
0cd5af3c 8955f4          mov     dword ptr [ebp-0Ch],edx
    //some debugger checks
0cd5af3f 833d9424760300  cmp     dword ptr ds:[3762494h],0
0cd5af46 7405            je      0cd5af4d  Branch
0cd5af48 e8e1cac25a      call    clr!JIT_DbgIsJustMyCode (67987a2e)
0cd5af4d c745fc00000000  mov     dword ptr [ebp-4],0
0cd5af54 90              nop

    //comparison or entity ref with  zero
0cd5af55 837df400        cmp     dword ptr [ebp-0Ch],0
0cd5af59 0f95c0          setne   al
0cd5af5c 0fb6c0          movzx   eax,al
0cd5af5f 8945fc          mov     dword ptr [ebp-4],eax
0cd5af62 837dfc00        cmp     dword ptr [ebp-4],0
    //if not zero, jump further
0cd5af66 7542            jne     0cd5afaa  Branch
    //throwing exception here      

这个问题的原因实际上是NDepend警告使用装箱/拆箱。我好奇它为什么在一些泛型类中发现了装箱,现在清楚了。


1
如果您使用object.Equals(entity, default(TEntity)),那么它是否相同? - Konrad Kokosa
我要在这里猜一个。如果我没记错,接口可以导致值类型的装箱,所以我想知道这是否与此有关。也许编译器认为必须将其装箱,以防止输入值类型以检查空引用。不过我可能是在胡说八道。:) 编辑:尽管它是一个“类”(而不是值类型)的约束,但我不确定编译器和/或CLR和/或IL是否考虑了这一点。 - Chris Sinclair
1
我还怀疑,object.Equals==运算符都是如此“通用”,以至于它们默认会进行装箱。尝试使用EqualityComparer<TEntity>.Default.Equals(entity, default(T)),因为它应该使用通用方法。 - Konrad Kokosa
进行了一些简化测试后,我怀疑这是“class”约束不影响编译后的IL结构的情况。它假设 TEntity 可能是值类型,并且当应用 == null(实际上是 Object.ReferenceEquals)时,需要将其装箱为 object。我怀疑当方法基于实际闭合泛型类型被JIT时,装箱和/或空检查可能会被优化,尽管现在我没有工具来检查这一点。只是一个猜测。 - Chris Sinclair
@ChrisSinclair - 谢谢。看起来约束条件没有被遵守,这很奇怪。csc 生成代码时应该意识到这一点。 - mikalai
显示剩余7条评论
2个回答

17
在审查生成BOX指令的C#编译器源代码时,我遇到了一个非常相关的评论。在fncbind.cpp源文件中,有这个注释,虽然与此特定代码不直接相关,但是很重要:

//注意: 对于标志,即使我们知道类型是引用类型,我们也必须使用EXF_FORCE_UNBOX(而不是EXF_REFCHECK)。验证程序期望所有类型参数代码表现为类型参数是值类型。 //jit应该能够聪明地处理它...

因此,这里存在这个注释是因为验证程序需要它。
是的,jit对此很聪明。它根本不为BOX指令发出任何代码。

这很有趣 - 所以决定简化IL方法,因为(很可能)JIT编译器中已经存在了取消装箱引用类型检查的方法。 - mikalai
啊,好发现。我非常努力地尝试以这种方式解释ECMA规范,但它看起来并不像规范要求的那样。相关引用:“无论运行时实际类型是值类型还是引用类型,验证跟踪的类型始终是泛型参数的“装箱”typeTok。” - Roman Starkov

12
ECMA规范对box指令的说明如下:

堆栈转换:..., val -> ..., obj

...

如果typeTok是一个泛型参数,则box指令的行为取决于运行时实际的类型。如果该类型[...]是引用类型,则val不会改变。

其意思是编译器可以假设将引用类型进行box是安全的。所以在泛型中,编译器有两个选择:发出保证无论如何泛型类型受到限制,代码都能正常工作的代码,或者优化代码并省略冗余指令,只要能证明它们是不必要的。
总体来讲,微软的C#编译器倾向于选择更简单的方法,将所有优化留给JIT阶段。在我看来,你的例子恰恰就是这样:没有优化某些内容,因为实现优化需要时间,而在实践中,保存这个box指令可能没有太大的价值。
C#甚至允许将一个无约束泛型类型值与null进行比较,因此编译器必须支持这种普遍情况。实现这个普遍情况最简单的方法是使用box指令,该指令能够处理引用、值和可空类型,并将引用或null值正确地推送到堆栈上。因此,编译器最容易做的事情就是不考虑限制发出box,然后将该值与零进行比较(brtrue)。

1
请注意Hans的答案,他似乎给出了这个指令存在的最终原因,并且应该被接受的答案。PEVerify就像是正确IL的权威,如果PEVerify出于任何原因拒绝某些内容,那么这些内容就和无效一样(在低信任度的情况下尤其重要)。 - Roman Starkov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接