38

Here is a simple generic type with a unique generic parameter constrained to reference types:

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

The generated IL by csc.exe is :

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

So each parameter is boxed before proceeding with the comparison.

But if the constraint indicates that "T" should never be a value type, why is the compiler trying to box r1 and r2 ?

Mehrdad Afshari
  • 414,610
  • 91
  • 852
  • 789
Pragmateek
  • 13,174
  • 9
  • 74
  • 108

2 Answers2

45

It's required to satisfy the verifiability constraints for the generated IL. Note that unverifiable doesn't necessarily mean incorrect. It works just fine without the box instruction as long as its security context allows running unverifiable code. Verification is conservative and is based on a fixed rule set (like reachability). To simplify things, they chose not to care about presence of generic type constraints in the verification algorithm.

Common Language Infrastructure Specification (ECMA-335)

Section 9.11: Constraints on generic parameters

... Constraints on a generic parameter only restrict the types that the generic parameter may be instantiated with. Verification (see Partition III) requires that a field, property or method that a generic parameter is known to provide through meeting a constraint, cannot be directly accessed/called via the generic parameter unless it is first boxed (see Partition III) or the callvirt instruction is prefixed with the constrained prefix instruction. ...

Removing the box instructions will result in unverifiable code:

.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

UPDATE (Answer to the comment): As I mentioned above, verifiability is not equivalent to correctness (here I'm talking about "correctness" from a type-safety point of view). Verifiable programs are a strict subset of correct programs (i.e. all verifiable programs are demonstrably correct, but there are correct programs that are not verifiable). Thus, verifiability is a stronger property than correctness. Since C# is a Turing-complete language, Rice's theorem states that proving that programs are correct is undecidable in general case.

Let's get back to my reachability analogy as it's easier to explain. Assume you were designing C#. One thing have thought about is when to issue a warning about unreachable code, and to remove that piece of code altogether in the optimizer, but how you are going to detect all unreachable code? Again, Rice's theorem says you can't do that for all programs. For instance:

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

This is something that C# compiler actually warns about. But it doesn't warn about:

bool Condition() {
   return true;
}

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

A human can prove that control flow never reaches that line in the latter case. One could argue that the compiler could statically prove DoSomething is unreachable in this case too, but it doesn't. Why? The point is you can't do that for all programs, so you should draw the line at some point. At this stage, you have to define a decidable property and call it "reachability". For instance, for reachability, C# sticks to constant expressions and won't look at the contents of the functions at all. Simplicity of analysis and design consistency are important goals in deciding where to draw the line.

Going back to our verifiability concept, it's a similar problem. Verifiability, unlike correctness, is a decidable property. As the runtime designer, you have to decide how to define verifiability, based on performance considerations, easy of implementation, ease of specification, consistency, making it easy for the compiler to confidently generate verifiable code. Like most design decisions, it involves a lot of trade-offs. Ultimately, the CLI designers have decided that they prefer not too look at generic constraints at all when they are checking for verifiability.

Mehrdad Afshari
  • 414,610
  • 91
  • 852
  • 789
  • @drachenstern: Done. Quoted the CLI spec. – Mehrdad Afshari Dec 18 '10 at 01:44
  • 1
    +1 What does a boxing operation even *do* on a reference type? Is it a no-op? – Ani Dec 18 '10 at 02:06
  • 3
    @Ani: Essentially. JIT will take care of removing it. – Mehrdad Afshari Dec 18 '10 at 02:07
  • Excellent, thanks. So the only execution-time overhead this introduces is on the JITter as opposed to the generated native code (therefore probably insignificant), I take it. – Ani Dec 18 '10 at 02:09
  • 1
    @Ani: Indeed -- and having JIT ignore that is not specific to generic type parameters that are explicitly constrained to be reference types, since JIT will generate a distinct instance of native code for `SomeClass` and `SomeClass` anyway. – Mehrdad Afshari Dec 18 '10 at 02:15
  • @Mehrdad : Thanks for your detailed response. But I still does not completely get the point : why would this code be unverifiable ? The IL metadata clearly state that "r1" and "r2" will be references. Moreover I'm not sure the reference you give to the specs directly applies to this case because here we are not accessing a member of "T" but simply using the comparison "ceq" instruction from outside it. – Pragmateek Dec 18 '10 at 11:15
  • 1
    @Serious: I updated the answer. Re correctness of spec reference: `ceq` is *accessing* the generic arguments so they have to get boxed, so it's exactly relevant to the issue. The verifiability issue arises from a type compatibility requirement for `ceq`. – Mehrdad Afshari Dec 18 '10 at 13:37
  • Thanks for your update that clarifies a lot things. So the fact that "the CLI designers have decided that they prefer not too look at generic constraints at all when they are checking for verifiability" is the answer to this requirement. As for the "type compatibility requirement for ceq", in this particular situation it should not be an issue because the same type "T" is used for the two parameters. But I guess this is too a design shortcut, not a technical requirement. – Pragmateek Dec 18 '10 at 14:16
  • 1
    @Serious: Yes, essentially, not looking at generic constraints for verifiability is the answer. Re "type compatibility" for `ceq` being a shortcut: "type compatibility" here is not only considered within the two operands. The `ceq` operation itself should be compatible with type `T` and that's not the case where `T` is an unboxed non-primitive struct. Comparing two arbitrary complex user defined struct instances with `ceq` is invalid and will throw `InvalidProgramException`. Boxing requirement eliminates this possibility. – Mehrdad Afshari Dec 18 '10 at 14:47
  • @Mehrdad : thanks again for taking the time to explain things with so much details. Could you provide a IL sample demonstrating that "Comparing two arbitrary complex user defined struct instances with ceq is invalid and will throw InvalidProgramException" and that boxing is mandatory, because the documentation is not really explicit ? – Pragmateek Dec 18 '10 at 15:52
  • 1
    @Serious: Sure. Take your own IL, remove the box instructions and the "class" generic constraint on the type, reassemble to a DLL and add a reference to it, in the new project, declare a complex struct (`struct Test {public int a,b,c,d,e,f; public string s;}`) and do `new A().F(new Test(), new Test {a=10});`. You should get `InvalidProgramException`. By the way, the documentation is quite clear on this matter. There's a table in the spec for type compatibility for binary comparison operators. – Mehrdad Afshari Dec 18 '10 at 20:02
  • Thanks, I happened to be 100% away from my computer all weekend. Have another sweet upvote :D – jcolebrand Dec 20 '10 at 17:07
16

Mehrdad's answer is quite good; I just wanted to add a couple points:

First, yes, in this case this is just to keep the verifier happy. The jitter should of course optimize away the boxing instruction since it is not meaningful to box a reference type.

However, there are cases where to keep the verifier happy we must introduce boxing instructions that are not optimized away. For example, if you say:

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

The C# compiler knows that in D.M, U can only be int. Nevertheless, in order to be verifiable there are situations where u must be boxed to object and then unboxed to int. The jitter does not always optimize away these; we've pointed out to the jitter team that this is a possible optimization, but the situation is so obscure that it is unlikely to cause a big win for many actual customers. There are bigger-bang-for-buck optimizations they could be spending their time on.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067