views:

130

answers:

3

I have some questions about the following code:

using System;

namespace ConsoleApplication2
{
    public struct Disposable : IDisposable
    {
        public void Dispose() { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (Test()) { }
        }

        static Disposable Test()
        {
            return new Disposable();
        }
    }
}

My questions are:

  • Will the using-statement that operates on the Disposable struct, returned from Test() box the struct, or not?
  • How can I find the answer myself?

To try to find out myself, I inspected the IL produced by the above code, and here's the IL for the Main(...) method:

.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] valuetype ConsoleApplication2.Disposable CS$3$0000)
    L_0000: call valuetype ConsoleApplication2.Disposable ConsoleApplication2.Program::Test()
    L_0005: stloc.0 
    L_0006: leave.s L_0016
    L_0008: ldloca.s CS$3$0000
    L_000a: constrained ConsoleApplication2.Disposable
    L_0010: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0015: endfinally 
    L_0016: ret 
    .try L_0006 to L_0008 finally handler L_0008 to L_0016
}

I suspect the call to the virtual method there, on L_0010 will introduce a boxing operation, but the actual box instruction is not here.

The reason I'm asking is that a while ago, probably 1-2 years, I saw online an "optimization" of the using-statement someone commented on. The case was where the using-statement was used as syntax for a short-time lock on an object, where the lock was acquired in the method, and a struct was returned, which when disposed of, would release the lock, code like this:

using (LockTheObject())
{
    // use the object
}

and the comment was that by changing the return type of the LockTheObject method from IDisposable to the actual struct used, boxing was avoided.

But I'm wondering if this is true, or still true.

Can anyone point me in the right direction? If, in order to see the box operation, I'll have to inspect the runtime assembly code, please show me an example of what to look for, I'm well versed in assembly code so that's not a problem, but nothing jumped out at me when I looked at that either.

+4  A: 

It appears as though any value type that gets put in a using statement will not be boxed. This appears to be a C# optimization as boxing is only omitted when a value type that implements IDisposable is in a using statement, not in any other context.

For more info please see The Using Statement And Disposable Value Types:

A while ago Ian Griffiths wrote about an improvement to his TimedLock class in which he changed it from a class to a struct. This change resulted in a value type that implements IDisposable. I had a nagging question in the back of my mind at the time that I quickly forgot about. The question is wouldn’t instances of that type get boxed when calling Dispose?

And also Oh No! Not the TimedLock Again!:

John Sands points out a flaw in the code I showed in a recent blog for using timeouts on locks without abandoning most of the convenience of C#'s lock keyword .

Andrew Hare
The interaction between constrained and callvirt was what I was looking for, thanks!
Lasse V. Karlsen
+1  A: 

Instance methods on a value type take a this parameter as their first argument similar to how instance methods on reference types do. However, the parameter in this case is a managed pointer to the object's data, not a reference to a boxed object. You might find it laid out in memory like this:

Unboxed object:
-----------------------------------------
|              DATA                     |
-----------------------------------------
 ^ managed pointer to struct

Boxed object:
------------------------------------------------------------
| GC/Object header |              [Boxed] DATA             |
------------------------------------------------------------
                    ^ The 'unbox' opcode gives a managed pointer to the boxed data
 ^ A *reference* to any instance of a reference type or boxed object, points here

DATA is the same in both of these cases¹.

Instance methods on a value type expect the managed pointer to data specifically so boxing the objects is not required. As you see above, the constrained opcode is used before the call. It tells the runtime that the following callvirt instruction is receiving a managed pointer to a ConsoleApplication2.Disposable struct instead of the object reference it normally receives. In doing so, the JIT can resolve the sealed overload of Dispose() implemented by the struct and call it directly without boxing the object. Without the constrained prefix, the object passed to the callvirt instruction would have to be an object reference, because the standard virtual call dynamic resolution procedure is based on the fact that the GC/Object header is always in the expected location - and yes, this would force boxing for value types.

¹ We'll go ahead and ignore Nullable<T> for now.

280Z28
+1  A: 

Andrew Hare's answer is correct; I just wanted to add an interesting extra note. The optimization we emit -- of using constrained callvirt to skip the boxing when possible -- is actually strictly speaking a violation of the C# specification. The spec states that the finally block we generate for a value type resource is:

     finally 
     {
         ((IDisposable)resource).Dispose();
     }

which clearly is a boxing conversion on a value type. It is possible to construct contrived scenarios in which the lack of boxing in the implementation is visible.

(Thanks are due to Vladimir Reshetnikov for pointing out this spec violation to me.)

Eric Lippert