1
public static bool Equal<T>(T value, T match) {
           return Equals(value, match);
       }

So the question is if T is int32 is there going to be boxing here or the compiler will choose the int32 Equals with no boxing?

Valentin Kuzub
  • 11,703
  • 7
  • 56
  • 93
  • @Rango So there will be boxing? Why you are answering the different question?=) – Valentin Kuzub Jan 17 '19 at 16:38
  • @LewsTherin - the signature of `object` is `bool Equals(object arg1);` which is not the same as the above defined method or the method that code is calling (which happens to be itself). – Igor Jan 17 '19 at 16:44
  • 4
    The short answer is: overload resolution will choose `public static bool Equals (object objA, object objB);` The arguments will be ints. Therefore they have to be boxed, because int-to-object is a boxing conversion. – Eric Lippert Jan 17 '19 at 16:47
  • @EricLippert thanks Eric ;) – Valentin Kuzub Jan 17 '19 at 16:48
  • @EricLippert I honestly don't know why my mind jumped to that; I was thinking about it all wrong. I need to learn more about the inner-workings of C#. – Lews Therin Jan 17 '19 at 16:49
  • @LewsTherin: If only there were a blog about that... :-) – Eric Lippert Jan 17 '19 at 16:50
  • You're welcome. To clarify the question, which "int32 equals" did you have in mind? – Eric Lippert Jan 17 '19 at 16:51
  • Also, maybe say what you're trying to accomplish here. Equality checking is very tricky in C#, and we can probably give you some tips on how to do it correctly if we know what you're trying to do. – Eric Lippert Jan 17 '19 at 16:55
  • @EricLippert We were just wondering whether there is a chance that some advanced level optimization could be going on here, but everything is working very straightforward. – Valentin Kuzub Jan 17 '19 at 16:58
  • 1
    Boxing always occurs even if you would use `return value.Equals(match)` because there is no constraint so `object` is assumed. If you'd make a constraint `where T: IEquatable` then boxing can be avoided. – Tim Schmelter Jan 17 '19 at 17:10
  • Intellisense will tell you what overload is being called, and what it's arguments are, although `object.Equals` only has two overloads, and only one with two parameters, so I don't know what method could be called other than the one accepting objects. – Servy Jan 17 '19 at 17:13
  • Right, the key thing to realize with generics is that generics are not C++ templates. A C++ template *recompiles all the code for every template specialization at compile time*. A generic compiles the code *once*, and then *generates optimized versions of that code at runtime*. But the optimized versions do *not* re-do overload resolution. If overload resolution chooses a method that takes `object` because that's the best it can do with an unconstrained `T`, then it will be the `object` version no matter what `T` is at runtime. – Eric Lippert Jan 17 '19 at 17:14
  • @Servy: I suspect that what the OP was imagining was that the jitter could recognize "Oh, I have a call to Equals that is passing two ints, let me optimize that down to a 32 bit comparison instruction". But that's not what happens. – Eric Lippert Jan 17 '19 at 17:14
  • @EricLippert The question specifically asks about which overload of equals is chosen, so I don't see a basis for assuming that the OP thought there was some runtime optimization by the JITTer. – Servy Jan 17 '19 at 17:17
  • @Servy: The basis is the OP saying "**We were just wondering whether there is a chance that some advanced level optimization could be going on here**". The fundamental question here is whether the boxing is optimized away. – Eric Lippert Jan 17 '19 at 17:21

1 Answers1

8

There has been some confusion in the comments to the original question and to Rango's (basically correct) answer, so I thought I'd clear those up.

First off, a note about how generics work in C#. Generics are not templates!

In C#, generics are compiled once by the C# compiler into generic IL, and that IL is then recompiled into specialized forms by the jitter. For example, if we have a method M<T>(T t), then the C# compiler will compile that method and its body once, into IL.

When the jitter comes along, a call to M<string>, M<object> or M<IEnumerable> will trigger exactly one compilation; the jitter is very clever and it can compile the body into a form where it works no matter what the type argument is, provided that the type argument is a reference type. But M<int> and M<double> will each be compiled into their own assembly code body.

Note that the jitter does not know the rules of C#, and C# does overload resolution. By the time the C# compiler generates the IL, the exact method for every method call has already been chosen. So if you have:

static bool X(object a, object b) => object.Equals(a, b);
static bool X(int a, int b) => a == b;
static bool M<T>(T v, T m) => X(v, m);

then overload resolution chooses X(object, object) and compiles the code as though you wrote:

static bool M<T>(T v, T m) => X((object)v, (object)m);

If T turns out to be int, then both ints are boxed to object.

Let me re-emphasize that. By the time we get to the jitter, we already know which X is going to be called; that decision was made at C# compile time. The C# compiler reasons "I've got two Ts, I do not know that they are convertible to int, so I've got to choose the object version".

This is in contrast to C++ template code, which re-compiles the code for each template instantiation, and re-does overload resolution.

So that answers the original question that was asked.

Now let's get into the weird details.

When jit compiling M<int>, is the jitter permitted to notice that M<int> calls X(object, object), which then calls object.Equals(object, object), which is known to compare two boxed ints for equality, and generate the code directly that compares the two ints in their unboxed form?

Yes, the jitter is permitted to perform that optimization.

Does it in practice perform that optimization?

Not to my knowledge. The jitter does perform some inlining optimizations, but to my knowledge it does not perform any inlinings that advanced.

Are there situations in which the jitter does in practice elide a boxing?

Yes!

Can you give some examples?

Sure thing. Consider the following terrible code:

struct S 
{
  public int x;
  public void M()
  {
      this.x += 1;
  }
}

When we do:

S s = whatever;
s.M();

What happens? this in a value type is equivalent to a parameter of type ref S. So we take a ref to s, pass it to M, and so on.

Now consider the following:

interface I
{
  void M();
}
struct S : I { /* body as before */ }

Now suppose we do this:

S s = whatever;
I i = s;
i.M();

What happens?

  • Converting s to I is a boxing conversion, so we allocate a box, make the box implement I, and make a copy of s in the box.
  • Calling i.M() passes the box as the receiver to the implementation of I in the box. That then takes a ref to the copy of s in the box, and passes that ref as the this to M.

All right, now comes the bit that will confuse the heck out of you.

void Q<T>(T t) where T : I
{
  t.M();
}
...
S s = whatever;
Q<S>(s);

Now what happens? Obviously we make a copy of s into t and there is no boxing; both are of type S. But: I.M is expecting a receiver of type I, and t is of type S. Do we have to do what we did before? Do we box t to a box that implements I, and then the box calls S.M with the this being a ref to the box?

No. The jitter generates code that elides the boxing and calls S.M directly with ref t as this.

What does this mean? This means that:

void Q<T>(T t) where T : I
{
  t.M();
}

and

void Q<T>(T t) where T : I
{
  I i = t;
  i.M();
}

Are different! The former mutates t because the boxing is skipped. The latter boxes and then mutates the box.

The takeaway here should be mutable value types are pure evil and you should avoid them at all costs. As we've seen, you can very easily get into situations where you think you should be mutating a copy, but you're mutating the original, or worse, situations where you think you are mutating an original, but you're mutating a copy.

What bizarre magic makes this work?

Use sharplab.io and disassemble the methods I've given into IL. Read the IL very carefully; if there is anything you don't understand, look it up. All the magical mechanisms that make this optimization work are well-documented.

Does the jitter always do this?

No! (As you would know if you read all the documentation as I just suggested.)

However, it is slightly tricky to construct a scenario where the optimization cannot be performed. I will leave that as a puzzle:

Write me a program where we have a struct type S that implements an interface I. We constrain type parameter T to I, and construct T with S, and pass in a T t. We call a method with t, as the receiver, and the jitter always causes the receiver to be boxed.

Hint: I predict that the called method's name has seven letters in it. Was I right?

Challenge #2: A question: Is it possible to also demonstrate that the boxing occurred using the same technique that I suggested before? (That technique being: show that a boxing must have occurred because a mutation happened to a copy, not to the original.

Are there scenarios where the jitter boxes unnecessarily?

Yes! When I was working on the compiler, the jitter did not optimize away "box T to O, immediately unbox O back to T" instruction sequences, and sometimes the C# compiler is required to generate such sequences to make the verifier happy. We requested that the optimization be implemented; I do not know if it ever was.

Can you give an example?

Sure. Suppose we have

class C<T>
{
  public virtual void M<U>(T t, U u) where U : T { }
}
class D : C<int>
{
  public override void M<U>(int t, U u)
  {

OK, now at this point you know that the only possible type for U is int, and so t should be assignable to u, and u should be assignable to t, right? But the CLR verifier does not see it that way, and you can then run into situations where the compiler must generate code that causes int to be boxed to object and then unboxed to U, which is int, so the round-trip is pointless.

What's the takeaway here?

  • Do not mutate value types.
  • Generics are not templates. Overload resolution only happens once.
  • The jitter works very hard to eliminate boxing in generics, but if a T is converted to object, then that T is really, truly converted to object.
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067