Where oh where is my "With...EndWith" statement?
ok this one might take some time to read so i apologise upfront; but i just had to get this off my chest ....
I do so miss the With ... End With strucutre that good old VB6 and VB.NET offers.
Why i hear you gasp in angst ... well for two reasons ...
1. I am LAZY !
instead of typing;
int a = object.PropertyA;
int b = object.PropertyB;
...
int z = object.PropertyZ;
using “With .. End With” i could simply type the following;
with object
int a = .PropertyA;
int b = .PropertyB;
...
int z = .PropertyZ;
end with
so by my rudementary math skill that saves me typing a lot of characters each time i want to use a property on the object.
ok not a compelling enough reason for you, how about this one then ...
2. With ... End With improves performance.
before all you C# purists out there choke and die or something, hear me out ...
Let's look at a classic example of accessing the rows collection in the DataTable class under a DataSet class:
ds.Tables("yada").rows
This generates the following;
callvirt instance class [System.Data]System.Data.DataTableCollection
[System.Data]System.Data.DataSet::get_Tables()
ldstr "yada"
callvirt instance class [System.Data]System.Data.DataTable
[System.Data]System.Data.DataTableCollection::get_Item(string)
callvirt instance class [System.Data]System.Data.DataRowCollection
[System.Data]System.Data.DataTable::get_Rows()
So accessing the rows collection in a data table object results in 3 calls to 3 property getters "get methods".
What happens if we do 3 accesses to the rows collection, will there be 9 calls to "get methods" generated?
Take a look at this code example:
ret = ds.Tables["yada"].Rows.Count;
if(ds.Tables["yada"].Rows.IsReadOnly) {}
ds.Tables["yada"].Rows.Clear();
simple calls to 3 different properties on the rows collection, but is it ...
what happens in the generated IL code?
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection
[System.Data]System.Data.DataSet::get_Tables()
ldstr "yada"
callvirt instance class [System.Data]System.Data.DataTable
[System.Data]System.Data.DataTableCollection::get_Item(string)
callvirt instance class [System.Data]System.Data.DataRowCollection
[System.Data]System.Data.DataTable::get_Rows()
callvirt instance int32 [System.Data]System.Data.InternalDataCollectionBase::get_Count()
stloc.1
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection
[System.Data]System.Data.DataSet::get_Tables()
ldstr "yada"
callvirt instance class [System.Data]System.Data.DataTable
[System.Data]System.Data.DataTableCollection::get_Item(string)
callvirt instance class [System.Data]System.Data.DataRowCollection
[System.Data]System.Data.DataTable::get_Rows()
callvirt instance bool [System.Data]System.Data.InternalDataCollectionBase::get_IsReadOnly()
brfalse.s IL_0066
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection
[System.Data]System.Data.DataSet::get_Tables()
ldstr "yada"
callvirt instance class [System.Data]System.Data.DataTable
[System.Data]System.Data.DataTableCollection::get_Item(string)
callvirt instance class [System.Data]System.Data.DataRowCollection
[System.Data]System.Data.DataTable::get_Rows()
callvirt instance void [System.Data]System.Data.DataRowCollection::Clear()
it actually calls the same methods 3 times.
What we want to do is to make sure that the "get methods" only get called once for all the access to the rows collection.
If we were in VB.NET this would be simple by using the with...end with statement.
With ds.Tables["yada"].Rows
Ret = .Count
If .IsReadOnly
End if
.Clear
End With
This creates an extra object in the methods local area, used by the IL code when executing.
.locals init ([0] class [System.Data]System.Data.DataSet ds,
[1] int32 i,
[2] class [System.Data]System.Data.DataRowCollection _Vb_t_ref_0)
The last local variable ([2]), is generated at compile time when the compiler finds the with statement in the VB.Net code.
The IL code now takes advantage of this extra local variable by passing the reference to the rows collection to that extra hidden variable in the locals.
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection
[System.Data]System.Data.DataSet::get_Tables()
ldstr "yada"
callvirt instance class [System.Data]System.Data.DataTable
[System.Data]System.Data.DataTableCollection::get_Item(string)
callvirt instance class [System.Data]System.Data.DataRowCollection
[System.Data]System.Data.DataTable::get_Rows()
stloc.2
When this has been done, instead of calling all the get methods, there will now be calls directly to the hidden variable
ldloc.2
callvirt instance int32 [System.Data]System.Data.InternalDataCollectionBase::get_Count()
stloc.1
ldloc.2
callvirt instance void [System.Data]System.Data.DataRowCollection::Clear()
nop
ldloc.2
callvirt instance bool [System.Data]System.Data.InternalDataCollectionBase::get_IsReadOnly()
brfalse.s IL_0044
nop
This IL code looks much better and has fewer method calls.
FYI, if you look at the IL code the "End With" statement generates you will see it even cleans up after itself
ldnull
stloc.2
Now the kind folks at C# decided not to give use this little beauty because they though us "real" programmers had no need for this ...
Here's a proposal to try mimic the behaviour:
DataRowCollection drc = ds.Tables["yada"].Rows;
ret = drc.Count;
if(drc.IsReadOnly) {}
drc.Clear();
drc = null;
Not really as nice looking in Visual Studios as the VB.NET equivalent, but this will do the exact same thing as the VB.NET with...end with statement.
But this was about performance i hear you complaining, not about how nice something looks that we don't really care about ... fair point;
so let's take a look at that now;
below is a simple performance test comparing the first and the last C# code examples.
class Class1
{
[STAThread]
static void Main(string[] args)
{
DoWork(10000);
DoWork(100000);
DoWork(1000000);
Console.ReadLine();
}
private static void DoWork(int iterations)
{
foo obj = new foo();
long deltaticks = 0;
// first way, without our "with...endwith" mimic.
Console.WriteLine("Executing using the properties");
deltaticks = obj.barslow(iterations);
Console.WriteLine("Time to execute " + iterations.ToString() + " times:" + deltaticks.ToString() + " ticks");
// second way, using our "with...endwith"
Console.WriteLine("Exectuing through an object");
deltaticks = obj.barfast(iterations);
Console.WriteLine("Time to execute " + iterations.ToString() + " times:" + deltaticks.ToString() + " ticks");
}
}
public class foo
{
public long barfast(int iterations)
{
DataSet ds = new DataSet();
int ret = 0;
long startticks = 0;
long deltaticks = 0;
ds.Tables.Add("yada");
startticks = System.DateTime.Now.Ticks;
DataRowCollection drc = ds.Tables["yada"].Rows;
for(int ix = 0;ix < iterations;ix++)
{
ret = drc.Count;
if(drc.IsReadOnly) {}
drc.Clear();
}
drc = null;
deltaticks = System.DateTime.Now.Ticks - startticks;
return deltaticks;
}
public long barslow(int iterations)
{
DataSet ds = new DataSet();
int ret = 0;
long startticks = 0;
long deltaticks = 0;
ds.Tables.Add("yada");
startticks = System.DateTime.Now.Ticks;
DataRowCollection drc = ds.Tables["yada"].Rows; //just to have the same overhead as the method above, but this is not used
for(int i = 0; i < iterations; i++)
{
ret = ds.Tables["yada"].Rows.Count;
if(ds.Tables["yada"].Rows.IsReadOnly) {}
ds.Tables["yada"].Rows.Clear();
}
deltaticks = System.DateTime.Now.Ticks - startticks;
return deltaticks;
}
}
The result from the test is shown in table 1,
Iterations | Through properties | Through object | Difference
10000 | 312568 | 156284 | 200%
100000 | 1875408 | 781420 | 240%
1000000 | 19535500 | 8751904 | 223%
Note that these figures are approx but it's fair to say that the difference averages around 220-260%.
so in summary;
i plead with Anders Hejlsberg to give us C# developers a proper "with..endwith" statement for the sake of my laziness, but more importantly for the laziness of my poor pc's processor!