Imitera VB.Nets with-kommando i C-sharp
Förord
Microsoft .Net Framework är en 100%-ig objektorienterad plattform. Detta betyder att objekt är lagrade hierarkiskt såsom objekt, egenskaper, fält och metoder som i vissa fall blir djupa nivåer.Innehåll
»»
»
»
»
Relaterade artiklar
» Grundkurs i C-Sharp - Arv» Grundkurs i C-Sharp - Gränssnitt
» Grundkurs i C-Sharp - Åtkomstmodifiering
Problemet
Ett klassiskt exempel är åtkomsten av rad-objektet i klassen för DataTable som finns i klassen DataSet.
dsMyData.Tables("jolt").rows
Detta kommer generera följande IL kod:
(IL är Common Language Runtimes assembler)
callvirt instance class [System.Data]System.Data.DataTableCollection [System.Data]System.Data.DataSet::get_Tables()
ldstr "jolt"
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()
Så för att komma åt rad-objektet (collection) i datatabells objektet så producerar detta tre "get methods" (property procedures).
- Kommer detta hända varje gång vi försöker göra en åtkomst mot denna collection i dess hierarkiska objekt?
- Vad händer om vi gör 3 anrop mot denna rad collection, kommer det då generera 9 anrop till "get methods"?
Vi gör en test
ret = ds.Tables["jolt"].Rows.Count;
if(ds.Tables["jolt"].Rows.IsReadOnly) {}
ds.Tables["jolt"].Rows.Clear();
Tre enkla anrop till tre olika egenskaper på rad collection. Det ser ut som så men vad händer i den genererade IL-koden?
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection [System.Data]System.Data.DataSet::get_Tables()
ldstr "jolt"
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 "jolt"
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 "jolt"
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()
Oj vad stökig IL-koden blev. Här ser vi att vi kallar på samma metod tre gånger. Även om detta är lågnivå IL kod som körs så ser detta ut som det är mycket overhead som utförs.
Lösningen
Vad vi vill göra är ju att "get methods" endast kallas en gång för alla anrop emot rad collection. I VB.Net kan det faktiskt göras med följande kommando:
With ds.Tables["jolt"].Rows
Ret = .Count
If .IsReadOnly
End if
.Clear
End With
Denna programkod kommer lagra ett extra objekt i metodens lokala area, osynligt för oss när vi programmerar - men används av IL koden när den exekveras.
.locals init ([0] class [System.Data]System.Data.DataSet ds,
[1] int32 i,
[2] class [System.Data]System.Data.DataRowCollection _Vb_t_ref_0)
Den sista lokala variabeln, ([2]) generas av kompilatorn när den stöter på With i programkoden. IL koden nyttjar nu denna gömda variabel genom att sätta en referens till rad kollektionen.
ldloc.0
callvirt instance class [System.Data]System.Data.DataTableCollection [System.Data]System.Data.DataSet::get_Tables()
ldstr "jolt"
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
När detta har gjorts anropas metoderna direkt genom den gömda objekt variabeln istället för att kalla på "get methods".
(för att läsas av lättare så har alla referenser med instruktioner till pekare tagits bort, men IL_0044 refererar till den okända instruktionen):
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
Detta är nu så som IL-koden ser ut. Det ser mycket bättre ut och har färre metodanrop. För information så ta en titt på IL koden som genereras av "End With".
ldnull
stloc.2
Detta frigör den lokalt gömda variabelns objekt.
Men hur gör man i C#
Men i C# kan vi inte använda "with" kommandot över huvudtaget. Då är frågan, hur gör vi för att försäkra oss att samma smidiga och renare lösning utförs på samma sätt i IL koden?Nå, vi får i alla fall ingen hjälp av att skapa ett extra gömt element, men vi kan definitivt addera en sådan själva i alla fall. Här är mitt förslag på hur man gör motsvarande manipulering för C#
DataRowCollection drc = ds.Tables["jolt"].Rows;
ret = drc.Count;
if(drc.IsReadOnly) {}
drc.Clear();
drc = null;
Kanske inte ser lika snyggt ut i Visual Studio's kodfönster men kommer generera exakt samma resultat, dock med en liten skillnad. I VB.Net exemplet var den lokala variabeln gömt och saknade läsbart namn, så är det inte i detta fallet med C#.
Vad blir det för prestandaökningar?
Jag har satt upp ett enkelt performancetest för att loopa igenom den första och den sista c# koden i de två ovanstående exemplen.
En notering innan vi börjar. När du sätter upp testen så loopar du metoden som räknar antalet ticks före och efter loopens "for"-sats. Detta gav verkligen ett intressant resultat. Den första testen visade att metoden alltid gav 10 gånger mer ticks än det andra alternativet. Jag tror det har att göra med JITC - (Just In Time Compiler). |
Sagt å gjort så skrev jag om testen så att resultatmätningen skulle bli oberoende av JITC och testen inkluderade en "foo" klass med två metoder - båda loopades x antal gånger:
public class foo
{
public long bar(int iterations)
{
DataSet ds = new DataSet();
int ret = 0;
long currentms = 0;
long resultms = 0;
ds.Tables.Add("jolt");
currentms = System.DateTime.Now.Ticks;
DataRowCollection drc = ds.Tables["jolt"].Rows;
for(int ix = 0;ix < iterations;ix++)
{
ret = drc.Count;
if(drc.IsReadOnly) {}
drc.Clear();
}
drc = null;
resultms = System.DateTime.Now.Ticks - currentms;
return resultms;
}
public long barlong(int iterations)
{
DataSet ds = new DataSet();
int ret = 0;
long currentms = 0;
long resultms = 0;
ds.Tables.Add("jolt");
DataRowCollection drc = ds.Tables["jolt"].Rows;
currentms = System.DateTime.Now.Ticks;
for(int ix = 0;ix < iterations;ix++)
{
ret = ds.Tables["jolt"].Rows.Count;
if(ds.Tables["jolt"].Rows.IsReadOnly) {}
ds.Tables["jolt"].Rows.Clear();
}
resultms = System.DateTime.Now.Ticks - currentms;
return resultms;
}
}
"foo" klassen instansierades från en enkel console-applikation och metoden kallades separat för att sedan rapportera tillbaks antalet ticks till consolen.
class GoWithPeace
{
[STAThread]
static void Main(string[] args)
{
foo obj = new foo();
long resultms = 0;
int iterations = 0;
if(args.Length > 0) { iterations = int.Parse(args[0]); }
// Accessing throug properties.
Console.WriteLine("Executing using the properties");
resultms = obj.barlong(iterations);
Console.WriteLine("Time to execute " + iterations.ToString() + " times:" + resultms.ToString() + " ticks");
// Accesing through en object.
Console.WriteLine("Exectuing throw an object");
resultms = obj.bar(iterations);
Console.WriteLine("Time to execute " + iterations.ToString() + " times:" + resultms.ToString() + " ticks");
}
Konsolapplikationen kompilerades med /OPTIMIZE flaggan för att försäkra att IL koden optimerades så bra som möjligt.
Nedan ser du ett ungefärligt resultat från testen visas i denna tabell:
Loopar properties ticks object ticks Skillnad
10.000 600912 600912 600%
100.000 600912 1301976 460%
1.000.000 600912 13220064 447%
Testen utfördes på en Dell Inspiron 8000 775Mhz/512mb, med Windows XP Pro och .Net Framework RTM.
Observera att dessa siffror är ett ungefärligt resultat men man kan säga att skillnaderna är någonstans inom intervallet 700-400%. Jag vill även påpeka att om du kör testerna på andra typer av klasser och hierarkier så kommer värdena att förändras. Kompilatorn gör olika optimeringar i IL koden om "get methods" beter sig olika om vi använder index eller inte. Men åter igen, prestandan är påtaglig så det är det värt.
Sammmanställning
Med förståelse och studerande av IL koden (som för övrigt är den samma för alla språk i .Net plattformen) så kan vi se vart kostnaderna av programmeringen ger utslag, i C# liksom VB.Net. Detta exempel använde vi en rutin för att uppnå samma förutsättning för att sedan även kunna applicera samma lösningar. På detta sätt har vi nu optimerat arbetet när anrop mot hierarkiska objekt utförs.
0 Kommentarer