Do C# generics prevent autoboxing of structs in this case?
Asked Answered
P

2

12

Usually, treating a struct S as an interface I will trigger autoboxing of the struct, which can have impacts on performance if done often. However, if I write a generic method taking a type parameter T : I and call it with an S, then will the compiler omit the boxing, since it knows the type S and does not have to use the interface?

This code shows my point:

interface I{
    void foo();
}

struct S : I {
    public void foo() { /* do something */ }
}

class Y {

    void doFoo(I i){
        i.foo();
    }
    void doFooGeneric<T>(T t) where T : I {
        t.foo(); // <--- Will an S be boxed here??
    }

    public static void Main(string[] args){
        S x;
        doFoo(x); // x is boxed
        doFooGeneric(x); // x is not boxed, at least not here, right?
    }

}

The doFoo method calls foo() on an object of type I, so once we call it with an S, that S will get boxed. The doFooGeneric method does the same thing. However, once we call it with an S, no autoboxing might be required, since the runtime knows how to call foo() on an S. But will this be done? Or will the runtime blindly box S to an I to call the interface method?

Proper answered 16/7, 2015 at 14:52 Comment(7)
Try adding the struct constraint - ie void doFooGeneric<T>(T t) where T : struct, I {Princely
FWIW, using the IsBoxed function from this answer says that it's not boxed. I don't know the details of why though.Shannonshanny
Related if not duplicate: #3033250 and specially this answer by Marc GravellRunyon
@JamesThorpe As far as I can see you cannot use that method to see if t is boxed in the t.foo() call.Dissociate
@JeppeStigNielsen You wouldn't... you'd do it inside doFooGeneric? Like this?Shannonshanny
@JamesThorpe That just shows that t is not boxed inside the method doFooGeneric. It does not show what happens when t.foo() executes. Suppose we wrote t.ToString() instead of t.foo(). That would box because our S does not override ToString. Or if we had t.GetType(), that would always box (GetType() is non-virtual so never overridden).Dissociate
@JeppeStigNielsen Hmm... I thought we were only finding out whether or not t was boxed in doFooGeneric... I think I need to go read some more...Shannonshanny
D
7
void doFooGeneric<T>(T t) where T : I {
    t.foo(); // <--- Will an S be boxed here??
}

Boxing will be avoided there!

The struct type S is sealed. For value type versions of the type parameter T to your method doFooGeneric above, the C# compiler gives code that calls the relevant struct member directly, without boxing.

Which is cool.

See Sameer's answer for some technical details.


OK, so I came up with an example of this. I will be interested in better examples if anyone has some:

using System;
using System.Collections.Generic;

namespace AvoidBoxing
{
  static class Program
  {
    static void Main()
    {
      var myStruct = new List<int> { 10, 20, 30, }.GetEnumerator();
      myStruct.MoveNext(); // moves to '10' in list

      //
      // UNCOMMENT ONLY *ONE* OF THESE CALLS:
      //

      //UseMyStruct(ref myStruct);
      //UseMyStructAndBox(ref myStruct);

      Console.WriteLine("After call, current is now: " + myStruct.Current); // 10 or 20?
    }

    static void UseMyStruct<T>(ref T myStruct) where T : IEnumerator<int>
    {
      myStruct.MoveNext();
    }

    static void UseMyStructAndBox<T>(ref T myStruct)
    {
      ((IEnumerator<int>)myStruct).MoveNext();
    }
  }
}

Here the type of myStruct is a mutable value-type which holds a reference back to the List<>, and also holds "counter" that remembers what index in the List<> we have reached until now.

I had to use ref, otherwise the value-type would be copied by value when passed into either of the methods!

When I uncomment the call to UseMyStruct (only), this method moves the "counter" inside our value type one position ahead. If it did that in a boxed copy of the value type, we would not see it in the original instance of the struct.

To see what the difference is with boxing, try the call UseMyStructAndBox instead (comment UseMyStruct again). It creates a box at the cast, and the MoveNext happens on a copy. So the output is different!


To those who are unhappy with (or confused by) the ref, just write out Current from within the method instead. Then we can get rid of ref. Example:

static void F<T>(T t) where T : IEnumerator<int>
{
  t.MoveNext(); // OK, not boxed
  Console.WriteLine(t.Current);
}

static void G<T>(T t) where T : IEnumerator<int>
{
  ((IEnumerator<int>)t).MoveNext(); // We said "Box!", it will box; 'Move' happens to a copy
  Console.WriteLine(t.Current);
}
Dissociate answered 16/7, 2015 at 14:58 Comment(3)
If so, then yes, that would be very cool. But are you sure? Habib's answer states exactly the opposite.Proper
@gexicide, no, my answer was wrong with respect to the the where T: I otherwise it was correct.Runyon
This optimization is extremely important for example for Array.Sort<int>. This should work.Baccy
H
3

Boxing will be avoided as Constrained Opcodes come to play in the second case.

Hatten answered 16/7, 2015 at 15:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.