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?
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?
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 int
s 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 thatM<int>
callsX(object, object)
, which then callsobject.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?
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.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?
T
is converted to object
, then that T
is really, truly converted to object
.