In the last post we looked at some of the potential uses for the Reflector application.
I didn't end up elaborating on the third reason I stated for the Reflector being a compelling tool - that it can be used to help optimize code based on the resultant IL created. For fun I played around with using the Reflector to compare similarly structured code, and thought I'd use this post to share my approach and the results.
Firstly, though, I recommend taking a look at this useful primer on MSIL.
Now, let’s take some nearly identical code, and compare the output from each. I’m using Reflector for this, but other .NET disassembly tools are available: the most common one being ILDASM (presumably short for "IL DisASseMbler"), which ships as part of Visual Studio. Reflector is a nicer tool for my purposes, as it shows descriptions of individual IL instructions as you hover over them (an invaluable feature for those of us with imperfect knowledge/memories :-).
Here’s the code – the idea for this came from a comment from this post, where an esteemed colleague suggested that the IL created by try(), finally (dispose), is equivalent to that created by using() (and the source code using the latter approach is clearly more elegant).
To test out this assertion, I created two simple functions in C#, the only difference being that the first makes use of try(), finally(), while the second uses using():
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
namespace TestLibrary
{
public class TestCommands
{
[CommandMethodAttribute("TEST")]
static public void Version1()
{
Database db =
HostApplicationServices.WorkingDatabase;
Transaction tr =
db.TransactionManager.StartTransaction();
try
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
Circle cir =
new Circle(
new Point3d(50, 50, 0),
new Vector3d(0, 0, 1),
10
);
btr.AppendEntity(cir);
tr.AddNewlyCreatedDBObject(cir, true);
tr.Commit();
}
finally
{
tr.Dispose();
}
}
[CommandMethodAttribute("TEST2")]
static public void Version2()
{
Database db =
HostApplicationServices.WorkingDatabase;
using (
Transaction tr =
db.TransactionManager.StartTransaction()
)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
Circle cir =
new Circle(
new Point3d(50, 50, 0),
new Vector3d(0, 0, 1),
10
);
btr.AppendEntity(cir);
tr.AddNewlyCreatedDBObject(cir, true);
tr.Commit();
}
}
}
}
Once the code is compiled into an assembly, I loaded it into Reflector, to compare the resultant IL of the respective functions.
Here's the IL output from Reflector for Version1 - the function with try(), finally():
[ Note: copying and pasting the output into HTML maintains the tooltips with descriptions for the various IL operations - that's cool! ]
.method public hidebysig static void Version1() cil managed
{
.custom instance void [acmgd]Autodesk.AutoCAD.Runtime.CommandMethodAttribute::.ctor(string) = ( string("TEST") )
.maxstack 5
.locals init (
[0] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database database1,
[1] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction transaction1,
[2] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTable table1,
[3] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord record1,
[4] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Circle circle1)
L_0000: nop
L_0001: call [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database [acdbmgd]Autodesk.AutoCAD.DatabaseServices.HostApplicationServices::get_WorkingDatabase()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.TransactionManager [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database::get_TransactionManager()
L_000d: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction [acdbmgd]Autodesk.AutoCAD.DatabaseServices.TransactionManager::StartTransaction()
L_0012: stloc.1
L_0013: nop
L_0014: ldloc.1
L_0015: ldloc.0
L_0016: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database::get_BlockTableId()
L_001b: ldc.i4.0
L_001c: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::GetObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId, [acdbmgd]Autodesk.AutoCAD.DatabaseServices.OpenMode)
L_0021: castclass [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTable
L_0026: stloc.2
L_0027: ldloc.1
L_0028: ldloc.2
L_0029: ldsfld string modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst) [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord::ModelSpace
L_002e: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.SymbolTable::get_Item(string)
L_0033: ldc.i4.1
L_0034: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::GetObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId, [acdbmgd]Autodesk.AutoCAD.DatabaseServices.OpenMode)
L_0039: castclass [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord
L_003e: stloc.3
L_003f: ldc.r8 50
L_0048: ldc.r8 50
L_0051: ldc.r8 0
L_005a: newobj instance void [acdbmgd]Autodesk.AutoCAD.Geometry.Point3d::.ctor(float64, float64, float64)
L_005f: ldc.r8 0
L_0068: ldc.r8 0
L_0071: ldc.r8 1
L_007a: newobj instance void [acdbmgd]Autodesk.AutoCAD.Geometry.Vector3d::.ctor(float64, float64, float64)
L_007f: ldc.r8 10
L_0088: newobj instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Circle::.ctor([acdbmgd]Autodesk.AutoCAD.Geometry.Point3d, [acdbmgd]Autodesk.AutoCAD.Geometry.Vector3d, float64)
L_008d: stloc.s circle1
L_008f: ldloc.3
L_0090: ldloc.s circle1
L_0092: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord::AppendEntity([acdbmgd]Autodesk.AutoCAD.DatabaseServices.Entity)
L_0097: pop
L_0098: ldloc.1
L_0099: ldloc.s circle1
L_009b: ldc.i4.1
L_009c: callvirt instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::AddNewlyCreatedDBObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject, bool)
L_00a1: nop
L_00a2: ldloc.1
L_00a3: callvirt instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::Commit()
L_00a8: nop
L_00a9: nop
L_00aa: leave.s L_00b6
L_00ac: nop
L_00ad: ldloc.1
L_00ae: callvirt instance void [acdbmgd]Autodesk.AutoCAD.Runtime.DisposableWrapper::Dispose()
L_00b3: nop
L_00b4: nop
L_00b5: endfinally
L_00b6: nop
L_00b7: ret
.try L_0013 to L_00ac finally handler L_00ac to L_00b6
} |
And here's the output for Version2 - the one with using():
.method public hidebysig static void Version2() cil managed
{
.custom instance void [acmgd]Autodesk.AutoCAD.Runtime.CommandMethodAttribute::.ctor(string) = ( string("TEST2") )
.maxstack 5
.locals init (
[0] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database database1,
[1] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction transaction1,
[2] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTable table1,
[3] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord record1,
[4] [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Circle circle1,
[5] bool flag1)
L_0000: nop
L_0001: call [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database [acdbmgd]Autodesk.AutoCAD.DatabaseServices.HostApplicationServices::get_WorkingDatabase()
L_0006: stloc.0
L_0007: ldloc.0
L_0008: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.TransactionManager [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database::get_TransactionManager()
L_000d: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction [acdbmgd]Autodesk.AutoCAD.DatabaseServices.TransactionManager::StartTransaction()
L_0012: stloc.1
L_0013: nop
L_0014: ldloc.1
L_0015: ldloc.0
L_0016: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Database::get_BlockTableId()
L_001b: ldc.i4.0
L_001c: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::GetObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId, [acdbmgd]Autodesk.AutoCAD.DatabaseServices.OpenMode)
L_0021: castclass [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTable
L_0026: stloc.2
L_0027: ldloc.1
L_0028: ldloc.2
L_0029: ldsfld string modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst) [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord::ModelSpace
L_002e: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.SymbolTable::get_Item(string)
L_0033: ldc.i4.1
L_0034: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::GetObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId, [acdbmgd]Autodesk.AutoCAD.DatabaseServices.OpenMode)
L_0039: castclass [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord
L_003e: stloc.3
L_003f: ldc.r8 50
L_0048: ldc.r8 50
L_0051: ldc.r8 0
L_005a: newobj instance void [acdbmgd]Autodesk.AutoCAD.Geometry.Point3d::.ctor(float64, float64, float64)
L_005f: ldc.r8 0
L_0068: ldc.r8 0
L_0071: ldc.r8 1
L_007a: newobj instance void [acdbmgd]Autodesk.AutoCAD.Geometry.Vector3d::.ctor(float64, float64, float64)
L_007f: ldc.r8 10
L_0088: newobj instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Circle::.ctor([acdbmgd]Autodesk.AutoCAD.Geometry.Point3d, [acdbmgd]Autodesk.AutoCAD.Geometry.Vector3d, float64)
L_008d: stloc.s circle1
L_008f: ldloc.3
L_0090: ldloc.s circle1
L_0092: callvirt instance [acdbmgd]Autodesk.AutoCAD.DatabaseServices.ObjectId [acdbmgd]Autodesk.AutoCAD.DatabaseServices.BlockTableRecord::AppendEntity([acdbmgd]Autodesk.AutoCAD.DatabaseServices.Entity)
L_0097: pop
L_0098: ldloc.1
L_0099: ldloc.s circle1
L_009b: ldc.i4.1
L_009c: callvirt instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::AddNewlyCreatedDBObject([acdbmgd]Autodesk.AutoCAD.DatabaseServices.DBObject, bool)
L_00a1: nop
L_00a2: ldloc.1
L_00a3: callvirt instance void [acdbmgd]Autodesk.AutoCAD.DatabaseServices.Transaction::Commit()
L_00a8: nop
L_00a9: nop
L_00aa: leave.s L_00be
L_00ac: ldloc.1
L_00ad: ldnull
L_00ae: ceq
L_00b0: stloc.s flag1
L_00b2: ldloc.s flag1
L_00b4: brtrue.s L_00bd
L_00b6: ldloc.1
L_00b7: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_00bc: nop
L_00bd: endfinally
L_00be: nop
L_00bf: ret
.try L_0013 to L_00ac finally handler L_00ac to L_00be
} |
So what's different?
Both functions compile into IL comprising try() & finally() blocks. The "using" function (Version2) has an additional local variable declared at the top (flag1, in location 5), which is not explicitly used in our source code, but is clearly used for something in the IL.
The main difference is in the finally() block. Here's what we see in Version2:
L_00ac: ldloc.1
L_00ad: ldnull
L_00ae: ceq
L_00b0: stloc.s flag1
L_00b2: ldloc.s flag1
L_00b4: brtrue.s L_00bd
L_00b6: ldloc.1
L_00b7: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_00bc: nop
L_00bd: endfinally
Here's what it does, instruction by instruction:
L_00ac: load the value of our transaction variable (tr in our source, transaction1 in the disassembled IL: the local variable in location 1) onto the evaluation stack
L_00ad: load the value "null" onto the evaluation stack
L_00ae: compare the top two values on the evaluation stack (are they equal?)
L_00b0: store the result of this comparison in flag1 (aha!)
L_00b2: push the value of flag1 back onto the eval stack
L_00b4: if the value of flag1 is "true" (i.e. tr == null), then jump to the end
L_00b6: load the value of the tr variable onto the eval stack
L_00b7: call Dispose on the transaction
etc.
This is functionally equivalent to the code in Version1, other than we have no explicit check for whether tr is null (and if we add it, which is actually a good idea, the resultant code would look different - give it a try, if you're interested in looking into this further).
The other difference is that Version1 uses Autodesk.AutoCAD.Runtime.DisposableWrapper::Dispose(), while Version2 uses System.IDisposable::Dispose(). Which is ultimately also an insignificant difference.
Hopefully this simple example shows how helpful Reflector is in taking the lid off .NET assemblies and how it can further our understanding of the way various .NET languages get compiled into MSIL (and therefore get executed by the .NET Framework).