lunedì 13 gennaio 2014

Lambda expressions - IL e blando performance test a pari condizioni di esecuzione - parte 1

Lamba expressions e IL

Era da tempo che mi chiedevo come il compilatore gestisse una funzione lambda. Esistono diverse possibilità al riguardo, tra cui una generazione a Runtime tramite dynamic methods. Oggi ho messo mano al disassemblatore .NET, alla ricerca di una risposta concreta.


IL? Questo sconosciuto...

Il funzionamento del .NET è solitamente quello di una macchina virtuale, molto simile ai concetti che fanno girare generazioni managed precedenti come quelle di Java. Il nostro codice, quando compilato con profilo non nativo, genera una serie di istruzioni di un linguaggio astratto comprensibile alla macchina virtuale, chiamato Intermediate Language e utilizzato in informatica per l'appunto per disgiungere completamente il livello hardware dal livello software e scrivere funzioni non necessariamente legate a una piattaforma ben definita. Le implementazioni della VM infatti decidono le infrastrutture su cui il codice può girare. Questa astrazione ha anche il vantaggio di permettere con buona approssimazione di livellare determinati comportamenti su tutte le generazioni di hardware che supportano la VM. Ad esempio, è possibile in linea di massima non tenere conto della dimensione degli indirizzamenti o dei registri della CPU. Dati complessi sono disponibili su tutte la VM che supportano l'IL, cambia praticamente solo la velocità di esecuzione, maggiore su hardware che supportano a livello fisico le funzionalità richieste.

Nel .NET l'intermediate Language è chiamato CIL (Common Intermediate Language). La lista degli OP CODES dell'IL Microsoft è disponibile all'interno della classe System.Reflection.Emit.OpCodes e viene usata per la compilazione dei dynamic methods, argomento che prima o poi tratterò.

IL e Lambda, la storia non è difficile.

Per verificare il comportamento del compilatore in presenza di una lambda a parità di condizioni di esecuzione ho scritto un progetto dedicato esclusivamente a mettere in evidenza le differenze di codice tramite disassemblatore. Il principio era abbastanza banale: scrivere due classi che facessero la stessa e medesima cosa, ovvero lanciassero un thread, ma in una definendo il delegate void mediante lambda, mentre nell'altra usando la classica dichiarazione di metodo.

Definisco "parità di condizioni di esecuzione" come la mancanza totale di violazioni dello scope delle variabili. Ovvero, variabili dichiarate nei metodi "padre" delle lambda non verranno utilizzate all'interno dei metodi figlio. Questa condizione richiederà un secondo test e la definizione di codice il più possibile oggettivo e confrontabile.

Invece, nei casi banali trattati in questo articolo il risultato anche se semplice è affascinante.

TaskWithLambda.cs


public class TaskWithLambda
{
    public void Run(bool async)
    {
        Task task = null;

        task = new Task(() =>
        {
            TimeSpan startTime;
            TimeSpan doubleStartTime;
            TimeSpan doubleEndTime;
            TimeSpan arrayStartTime;
            TimeSpan arrayEndTime;
            TimeSpan endTime;
            List<double> values = null;
            double[] source = null;
            double[] destination = null;
            Random rnd = null;
            StringBuilder testResult = null;
            int maxIteractions = 2500000;

            startTime = TimeSpan.FromTicks(Environment.TickCount);
            doubleStartTime = TimeSpan.FromTicks(Environment.TickCount);
            values = new List<double>();

            for (double i = 0; i < maxIteractions; i++)
            {
                double res = 0;
                res = Math.Sin(i);
                values.Add(res);
            }

            doubleEndTime = TimeSpan.FromTicks(Environment.TickCount);
            arrayStartTime = TimeSpan.FromTicks(Environment.TickCount);

            rnd = new Random();
            source = new Double[maxIteractions];

            for (int i = 0; i < maxIteractions; i++)
            {
                double dvalue = 0;
                int ivalue = 0;
                double v = 0;

                ivalue = rnd.Next();
                dvalue = rnd.NextDouble();
                v = dvalue * ivalue;
                source[i] = v;
            }

            destination = new double[maxIteractions];
            source.CopyTo(destination, 0);
            arrayEndTime = TimeSpan.FromTicks(Environment.TickCount);
            endTime = TimeSpan.FromTicks(Environment.TickCount);

            testResult = new StringBuilder();
            testResult.AppendLine("Task with Lambda");
            testResult.AppendLine();
            testResult.AppendFormat("Test start: {0}", startTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Test end: {0}", endTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Duration delta: {0}", (endTime - startTime).ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Sin test start ({0} sin(x) calculations): {1}", maxIteractions, doubleStartTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Sin test end ({0} sin(x) calculations): {1}", maxIteractions, doubleEndTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Sin test delta ({0} sin(x) calculations): {1}", maxIteractions, (doubleEndTime - doubleStartTime).ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Random typeof(double) array initialization and copy ({0} elements) start: {1}",
                                    maxIteractions,
                                    arrayStartTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Random typeof(double) array initialization and copy (50.000 elements) end: {1}",
                                    maxIteractions,
                                    arrayEndTime.ToString());
            testResult.AppendLine();
            testResult.AppendFormat("Random typeof(double) array initialization and copy (50.000 elements) delta: {1}",
                                    maxIteractions,
                                    (arrayEndTime - arrayStartTime).ToString());
            testResult.AppendLine();

            Console.WriteLine(testResult.ToString());
            Console.WriteLine();
        });

        task.Start();

        if (!async)
            task.Wait();
    }
}

La classe è abbastanza semplice. Il metodo Run lancia in maniera sincrona il task che esegue un paio di computazioni trigonometriche e uno spostamento di vettore lineare.

IL TaskWithLambda::Run : vod<bool>


.method public hidebysig instance void  Run(bool async) cil managed
{
  // Code size       60 (0x3c)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Threading.Tasks.Task task,
           [1] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  stloc.0
  IL_0003:  ldsfld     class [mscorlib]System.Action LambdaILTest.TaskWithLambda::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0008:  brtrue.s   IL_001d
  IL_000a:  ldnull
  IL_000b:  ldftn      void LambdaILTest.TaskWithLambda::'b__0'()
  IL_0011:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_0016:  stsfld     class [mscorlib]System.Action LambdaILTest.TaskWithLambda::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_001b:  br.s       IL_001d
  IL_001d:  ldsfld     class [mscorlib]System.Action LambdaILTest.TaskWithLambda::'CS$<>9__CachedAnonymousMethodDelegate1'
  IL_0022:  newobj     instance void [mscorlib]System.Threading.Tasks.Task::.ctor(class [mscorlib]System.Action)
  IL_0027:  stloc.0
  IL_0028:  ldloc.0
  IL_0029:  callvirt   instance void [mscorlib]System.Threading.Tasks.Task::Start()
  IL_002e:  nop
  IL_002f:  ldarg.1
  IL_0030:  stloc.1
  IL_0031:  ldloc.1
  IL_0032:  brtrue.s   IL_003b
  IL_0034:  ldloc.0
  IL_0035:  callvirt   instance void [mscorlib]System.Threading.Tasks.Task::Wait()
  IL_003a:  nop
  IL_003b:  ret
} // end of method TaskWithLambda::Run

Il disassemblatore mette subito in evidenza la dimensione del codice. Abbiamo 60 istruzioni nel metodo. E si capisce subito la politica del compilatore rispetto alle lambda. La nostra funzione anonima infatti viene dichiarata all'interno della classe come un qualsiasi altro metodo come è visibile in questa immagine:



e il metodo lambda è il seguente, chiamato dal compilatore <Run>b_0:

IL TaskWithLambda::<Run>b_0 : void()


.method private hidebysig static void  '<Run>b__0'() cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       720 (0x2d0)
  .maxstack  5
  .locals init ([0] valuetype [mscorlib]System.TimeSpan startTime,
           [1] valuetype [mscorlib]System.TimeSpan doubleStartTime,
           [2] valuetype [mscorlib]System.TimeSpan doubleEndTime,
           [3] valuetype [mscorlib]System.TimeSpan arrayStartTime,
           [4] valuetype [mscorlib]System.TimeSpan arrayEndTime,
           [5] valuetype [mscorlib]System.TimeSpan endTime,
           [6] class [mscorlib]System.Collections.Generic.List`1<float64> values,
           [7] float64[] source,
           [8] float64[] destination,
           [9] class [mscorlib]System.Random rnd,
           [10] class [mscorlib]System.Text.StringBuilder testResult,
           [11] int32 maxIteractions,
           [12] float64 i,
           [13] float64 res,
           [14] int32 V_14,
           [15] float64 dvalue,
           [16] int32 ivalue,
           [17] float64 v,
           [18] bool CS$4$0000,
           [19] valuetype [mscorlib]System.TimeSpan CS$0$0001)
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  stloc.s    values
  IL_0004:  ldnull
  IL_0005:  stloc.s    source
  IL_0007:  ldnull
  IL_0008:  stloc.s    destination
  IL_000a:  ldnull
  IL_000b:  stloc.s    rnd
  IL_000d:  ldnull
  IL_000e:  stloc.s    testResult
  IL_0010:  ldc.i4     0x2625a0
  IL_0015:  stloc.s    maxIteractions
  IL_0017:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_001c:  conv.i8
  IL_001d:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_0022:  stloc.0
  IL_0023:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_0028:  conv.i8
  IL_0029:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_002e:  stloc.1
  IL_002f:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<float64>::.ctor()
  IL_0034:  stloc.s    values
  IL_0036:  ldc.r8     0.0
  IL_003f:  stloc.s    i
  IL_0041:  br.s       IL_0071
  IL_0043:  nop
  IL_0044:  ldc.r8     0.0
  IL_004d:  stloc.s    res
  IL_004f:  ldloc.s    i
  IL_0051:  call       float64 [mscorlib]System.Math::Sin(float64)
  IL_0056:  stloc.s    res
  IL_0058:  ldloc.s    values
  IL_005a:  ldloc.s    res
  IL_005c:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<float64>::Add(!0)
  IL_0061:  nop
  IL_0062:  nop
  IL_0063:  ldloc.s    i
  IL_0065:  ldc.r8     1.
  IL_006e:  add
  IL_006f:  stloc.s    i
  IL_0071:  ldloc.s    i
  IL_0073:  ldloc.s    maxIteractions
  IL_0075:  conv.r8
  IL_0076:  clt
  IL_0078:  stloc.s    CS$4$0000
  IL_007a:  ldloc.s    CS$4$0000
  IL_007c:  brtrue.s   IL_0043
  IL_007e:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_0083:  conv.i8
  IL_0084:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_0089:  stloc.2
  IL_008a:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_008f:  conv.i8
  IL_0090:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_0095:  stloc.3
  IL_0096:  newobj     instance void [mscorlib]System.Random::.ctor()
  IL_009b:  stloc.s    rnd
  IL_009d:  ldloc.s    maxIteractions
  IL_009f:  newarr     [mscorlib]System.Double
  IL_00a4:  stloc.s    source
  IL_00a6:  ldc.i4.0
  IL_00a7:  stloc.s    V_14
  IL_00a9:  br.s       IL_00ed
  IL_00ab:  nop
  IL_00ac:  ldc.r8     0.0
  IL_00b5:  stloc.s    dvalue
  IL_00b7:  ldc.i4.0
  IL_00b8:  stloc.s    ivalue
  IL_00ba:  ldc.r8     0.0
  IL_00c3:  stloc.s    v
  IL_00c5:  ldloc.s    rnd
  IL_00c7:  callvirt   instance int32 [mscorlib]System.Random::Next()
  IL_00cc:  stloc.s    ivalue
  IL_00ce:  ldloc.s    rnd
  IL_00d0:  callvirt   instance float64 [mscorlib]System.Random::NextDouble()
  IL_00d5:  stloc.s    dvalue
  IL_00d7:  ldloc.s    dvalue
  IL_00d9:  ldloc.s    ivalue
  IL_00db:  conv.r8
  IL_00dc:  mul
  IL_00dd:  stloc.s    v
  IL_00df:  ldloc.s    source
  IL_00e1:  ldloc.s    V_14
  IL_00e3:  ldloc.s    v
  IL_00e5:  stelem.r8
  IL_00e6:  nop
  IL_00e7:  ldloc.s    V_14
  IL_00e9:  ldc.i4.1
  IL_00ea:  add
  IL_00eb:  stloc.s    V_14
  IL_00ed:  ldloc.s    V_14
  IL_00ef:  ldloc.s    maxIteractions
  IL_00f1:  clt
  IL_00f3:  stloc.s    CS$4$0000
  IL_00f5:  ldloc.s    CS$4$0000
  IL_00f7:  brtrue.s   IL_00ab
  IL_00f9:  ldloc.s    maxIteractions
  IL_00fb:  newarr     [mscorlib]System.Double
  IL_0100:  stloc.s    destination
  IL_0102:  ldloc.s    source
  IL_0104:  ldloc.s    destination
  IL_0106:  ldc.i4.0
  IL_0107:  callvirt   instance void [mscorlib]System.Array::CopyTo(class [mscorlib]System.Array,
                                                                    int32)
  IL_010c:  nop
  IL_010d:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_0112:  conv.i8
  IL_0113:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_0118:  stloc.s    arrayEndTime
  IL_011a:  call       int32 [mscorlib]System.Environment::get_TickCount()
  IL_011f:  conv.i8
  IL_0120:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::FromTicks(int64)
  IL_0125:  stloc.s    endTime
  IL_0127:  newobj     instance void [mscorlib]System.Text.StringBuilder::.ctor()
  IL_012c:  stloc.s    testResult
  IL_012e:  ldloc.s    testResult
  IL_0130:  ldstr      "Task with Lambda"
  IL_0135:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine(string)
  IL_013a:  pop
  IL_013b:  ldloc.s    testResult
  IL_013d:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0142:  pop
  IL_0143:  ldloc.s    testResult
  IL_0145:  ldstr      "Test start: {0}"
  IL_014a:  ldloca.s   startTime
  IL_014c:  constrained. [mscorlib]System.TimeSpan
  IL_0152:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0157:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object)
  IL_015c:  pop
  IL_015d:  ldloc.s    testResult
  IL_015f:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0164:  pop
  IL_0165:  ldloc.s    testResult
  IL_0167:  ldstr      "Test end: {0}"
  IL_016c:  ldloca.s   endTime
  IL_016e:  constrained. [mscorlib]System.TimeSpan
  IL_0174:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0179:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object)
  IL_017e:  pop
  IL_017f:  ldloc.s    testResult
  IL_0181:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0186:  pop
  IL_0187:  ldloc.s    testResult
  IL_0189:  ldstr      "Duration delta: {0}"
  IL_018e:  ldloc.s    endTime
  IL_0190:  ldloc.0
  IL_0191:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::op_Subtraction(valuetype [mscorlib]System.TimeSpan,
                                                                                                     valuetype [mscorlib]System.TimeSpan)
  IL_0196:  stloc.s    CS$0$0001
  IL_0198:  ldloca.s   CS$0$0001
  IL_019a:  constrained. [mscorlib]System.TimeSpan
  IL_01a0:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_01a5:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object)
  IL_01aa:  pop
  IL_01ab:  ldloc.s    testResult
  IL_01ad:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_01b2:  pop
  IL_01b3:  ldloc.s    testResult
  IL_01b5:  ldstr      "Sin test start ({0} sin(x) calculations): {1}"
  IL_01ba:  ldloc.s    maxIteractions
  IL_01bc:  box        [mscorlib]System.Int32
  IL_01c1:  ldloca.s   doubleStartTime
  IL_01c3:  constrained. [mscorlib]System.TimeSpan
  IL_01c9:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_01ce:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_01d3:  pop
  IL_01d4:  ldloc.s    testResult
  IL_01d6:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_01db:  pop
  IL_01dc:  ldloc.s    testResult
  IL_01de:  ldstr      "Sin test end ({0} sin(x) calculations): {1}"
  IL_01e3:  ldloc.s    maxIteractions
  IL_01e5:  box        [mscorlib]System.Int32
  IL_01ea:  ldloca.s   doubleEndTime
  IL_01ec:  constrained. [mscorlib]System.TimeSpan
  IL_01f2:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_01f7:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_01fc:  pop
  IL_01fd:  ldloc.s    testResult
  IL_01ff:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0204:  pop
  IL_0205:  ldloc.s    testResult
  IL_0207:  ldstr      "Sin test delta ({0} sin(x) calculations): {1}"
  IL_020c:  ldloc.s    maxIteractions
  IL_020e:  box        [mscorlib]System.Int32
  IL_0213:  ldloc.2
  IL_0214:  ldloc.1
  IL_0215:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::op_Subtraction(valuetype [mscorlib]System.TimeSpan,
                                                                                                     valuetype [mscorlib]System.TimeSpan)
  IL_021a:  stloc.s    CS$0$0001
  IL_021c:  ldloca.s   CS$0$0001
  IL_021e:  constrained. [mscorlib]System.TimeSpan
  IL_0224:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0229:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_022e:  pop
  IL_022f:  ldloc.s    testResult
  IL_0231:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0236:  pop
  IL_0237:  ldloc.s    testResult
  IL_0239:  ldstr      "Random typeof(double) array initialization and cop"
  + "y ({0} elements) start: {1}"
  IL_023e:  ldloc.s    maxIteractions
  IL_0240:  box        [mscorlib]System.Int32
  IL_0245:  ldloca.s   arrayStartTime
  IL_0247:  constrained. [mscorlib]System.TimeSpan
  IL_024d:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_0252:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_0257:  pop
  IL_0258:  ldloc.s    testResult
  IL_025a:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_025f:  pop
  IL_0260:  ldloc.s    testResult
  IL_0262:  ldstr      "Random typeof(double) array initialization and cop"
  + "y (50.000 elements) end: {1}"
  IL_0267:  ldloc.s    maxIteractions
  IL_0269:  box        [mscorlib]System.Int32
  IL_026e:  ldloca.s   arrayEndTime
  IL_0270:  constrained. [mscorlib]System.TimeSpan
  IL_0276:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_027b:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_0280:  pop
  IL_0281:  ldloc.s    testResult
  IL_0283:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_0288:  pop
  IL_0289:  ldloc.s    testResult
  IL_028b:  ldstr      "Random typeof(double) array initialization and cop"
  + "y (50.000 elements) delta: {1}"
  IL_0290:  ldloc.s    maxIteractions
  IL_0292:  box        [mscorlib]System.Int32
  IL_0297:  ldloc.s    arrayEndTime
  IL_0299:  ldloc.3
  IL_029a:  call       valuetype [mscorlib]System.TimeSpan [mscorlib]System.TimeSpan::op_Subtraction(valuetype [mscorlib]System.TimeSpan,
                                                                                                     valuetype [mscorlib]System.TimeSpan)
  IL_029f:  stloc.s    CS$0$0001
  IL_02a1:  ldloca.s   CS$0$0001
  IL_02a3:  constrained. [mscorlib]System.TimeSpan
  IL_02a9:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_02ae:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendFormat(string,
                                                                                                                            object,
                                                                                                                            object)
  IL_02b3:  pop
  IL_02b4:  ldloc.s    testResult
  IL_02b6:  callvirt   instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::AppendLine()
  IL_02bb:  pop
  IL_02bc:  ldloc.s    testResult
  IL_02be:  callvirt   instance string [mscorlib]System.Object::ToString()
  IL_02c3:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_02c8:  nop
  IL_02c9:  call       void [mscorlib]System.Console::WriteLine()
  IL_02ce:  nop
  IL_02cf:  ret
} // end of method TaskWithLambda::'<run>b__0'

che è del tutto equivalente al metodo che abbiamo scritto nella classe senza lambda. Genera infatti 720 istruzioni IL.

La differenza tra la classe che utilizza la funzione anonima e quella che non la utilizza è che all'interno del metodo Run è possibile osservare una assegnazione a un delegate di tipo Action il valore dell'indirizzo del metodo nascosto b_0, per poi passare il delegate come parametro alla classe Task.

IL TaskWithoutLambda:: Run : void(bool)


.method public hidebysig instance void  Run(bool async) cil managed
{
  // Code size       41 (0x29)
  .maxstack  2
  .locals init ([0] class [mscorlib]System.Threading.Tasks.Task task,
           [1] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  stloc.0
  IL_0003:  ldarg.0
  IL_0004:  ldftn      instance void LambdaILTest.TaskWithoutLambda::TestTaskOperation()
  IL_000a:  newobj     instance void [mscorlib]System.Action::.ctor(object,
                                                                    native int)
  IL_000f:  newobj     instance void [mscorlib]System.Threading.Tasks.Task::.ctor(class [mscorlib]System.Action)
  IL_0014:  stloc.0
  IL_0015:  ldloc.0
  IL_0016:  callvirt   instance void [mscorlib]System.Threading.Tasks.Task::Start()
  IL_001b:  nop
  IL_001c:  ldarg.1
  IL_001d:  stloc.1
  IL_001e:  ldloc.1
  IL_001f:  brtrue.s   IL_0028
  IL_0021:  ldloc.0
  IL_0022:  callvirt   instance void [mscorlib]System.Threading.Tasks.Task::Wait()
  IL_0027:  nop
  IL_0028:  ret
} // end of method TaskWithoutLambda::Run

Il metodo è praticamente identico a meno dell'assegnazione del delegate. Abbiamo 41 istruzioni IL contro le 60 del metodo con lambda.

Differenze tra TaskWithLambda e TaskWithtoutLambda

Riepilogando le differenze, la classe che fa uso della lambda genera un campo statico privato in più che rappresenta il delegate del metodo lambda, utilizzato all'accesso della funzione anonima da parte di altre classi o metodi.
Il metodo Run genera 60 istruzioni nel metodo con lambda, contro le 41 del metodo senza lambda.
Il metodo lambda ha la stessa dimensione (720 istruzioni) sia nella classe dove è dichiarato come funzione anonima sia dove è dichiarato come metodo di classe.

Test di esecuzione del codice

Ho effettuato per quanto possibile un test di velocità di esecuzione. Vengono utilizzate sia la classe Task che la classe Thread. Probabilmente il test non è preciso al millesimo in quanto mi sono basato sui Tick esposti dall'OS e quindi dipendente dal clock interno di Windows, ma mi è sembrato il metodo più conveniente per misurare la velocità in questa casistica.

Ho effettuato 3 lanci dell'applicativo. I risultati sono i seguenti.

-- Test 1

CPU utilizzate nel test: 

Intel(R) Core(TM) i7-2675QM CPU @ 2.20GHz
Architettura: x64
Famiglia: Intel64 Family 6 Model 42 Stepping 7
Current clock: 2200
Max clock:2200
Numero di cores:4
----

Task with Lambda

Test start: 00:02:24.3435109
Test end: 00:02:24.3435343
Duration delta: 00:00:00.0000234
Sin test start (2500000 sin(x) calculations): 00:02:24.3435109
Sin test end (2500000 sin(x) calculations): 00:02:24.3435250
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000141
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3435250
Random typeof(double) array initialization and copy (50.000 elements) end: 00:02:24.3435343
Random typeof(double) array initialization and copy (50.000 elements) delta: 00:00:00.0000093


Task without Lambda

Test start: 00:02:24.3435343
Test end: 00:02:24.3435593
Duration delta: 00:00:00.0000250
Sin test start (2500000 sin(x) calculations): 00:02:24.3435343
Sin test end (2500000 sin(x) calculations): 00:02:24.3435515
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000172
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3435515
Random typeof(double) array initialization and copy (2500000 elements) end: 00:02:24.3435593
Random typeof(double) array initialization and copy (2500000 elements) delta: 00:00:00.0000078


Thread with Lambda

Test start: 00:02:24.3435593
Test end: 00:02:24.3435718
Duration delta: 00:00:00.0000125
Sin test start (1500000 sin(x) calculations): 00:02:24.3435593
Sin test end (1500000 sin(x) calculations): 00:02:24.3435671
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000078
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3435671
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3435718
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000047


Thread without Lambda

Test start: 00:02:24.3435734
Test end: 00:02:24.3435859
Duration delta: 00:00:00.0000125
Sin test start (1500000 sin(x) calculations): 00:02:24.3435734
Sin test end (1500000 sin(x) calculations): 00:02:24.3435812
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000078
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3435812
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3435859
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000047


-- Test 2

CPU utilizzate nel test: 

Intel(R) Core(TM) i7-2675QM CPU @ 2.20GHz
Architettura: x64
Famiglia: Intel64 Family 6 Model 42 Stepping 7
Current clock: 2200
Max clock:2200
Numero di cores:4
----

Task with Lambda

Test start: 00:02:24.3443000
Test end: 00:02:24.3443234
Duration delta: 00:00:00.0000234
Sin test start (2500000 sin(x) calculations): 00:02:24.3443000
Sin test end (2500000 sin(x) calculations): 00:02:24.3443140
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000140
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3443140
Random typeof(double) array initialization and copy (50.000 elements) end: 00:02:24.3443234
Random typeof(double) array initialization and copy (50.000 elements) delta: 00:00:00.0000094


Task without Lambda

Test start: 00:02:24.3443234
Test end: 00:02:24.3443484
Duration delta: 00:00:00.0000250
Sin test start (2500000 sin(x) calculations): 00:02:24.3443234
Sin test end (2500000 sin(x) calculations): 00:02:24.3443406
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000172
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3443406
Random typeof(double) array initialization and copy (2500000 elements) end: 00:02:24.3443484
Random typeof(double) array initialization and copy (2500000 elements) delta: 00:00:00.0000078


Thread with Lambda

Test start: 00:02:24.3443484
Test end: 00:02:24.3443625
Duration delta: 00:00:00.0000141
Sin test start (1500000 sin(x) calculations): 00:02:24.3443484
Sin test end (1500000 sin(x) calculations): 00:02:24.3443578
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000094
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3443578
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3443625
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000047


Thread without Lambda

Test start: 00:02:24.3443625
Test end: 00:02:24.3443765
Duration delta: 00:00:00.0000140
Sin test start (1500000 sin(x) calculations): 00:02:24.3443625
Sin test end (1500000 sin(x) calculations): 00:02:24.3443703
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000078
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3443703
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3443765
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000062


-- Test 3

CPU utilizzate nel test: 

Intel(R) Core(TM) i7-2675QM CPU @ 2.20GHz
Architettura: x64
Famiglia: Intel64 Family 6 Model 42 Stepping 7
Current clock: 2200
Max clock:2200
Numero di cores:4
----

Task with Lambda

Test start: 00:02:24.3446203
Test end: 00:02:24.3446437
Duration delta: 00:00:00.0000234
Sin test start (2500000 sin(x) calculations): 00:02:24.3446203
Sin test end (2500000 sin(x) calculations): 00:02:24.3446343
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000140
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3446343
Random typeof(double) array initialization and copy (50.000 elements) end: 00:02:24.3446437
Random typeof(double) array initialization and copy (50.000 elements) delta: 00:00:00.0000094


Task without Lambda

Test start: 00:02:24.3446437
Test end: 00:02:24.3446703
Duration delta: 00:00:00.0000266
Sin test start (2500000 sin(x) calculations): 00:02:24.3446437
Sin test end (2500000 sin(x) calculations): 00:02:24.3446609
Sin test delta (2500000 sin(x) calculations): 00:00:00.0000172
Random typeof(double) array initialization and copy (2500000 elements) start: 00:02:24.3446609
Random typeof(double) array initialization and copy (2500000 elements) end: 00:02:24.3446703
Random typeof(double) array initialization and copy (2500000 elements) delta: 00:00:00.0000094


Thread with Lambda

Test start: 00:02:24.3446703
Test end: 00:02:24.3446843
Duration delta: 00:00:00.0000140
Sin test start (1500000 sin(x) calculations): 00:02:24.3446703
Sin test end (1500000 sin(x) calculations): 00:02:24.3446796
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000093
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3446796
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3446843
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000047


Thread without Lambda

Test start: 00:02:24.3446843
Test end: 00:02:24.3446984
Duration delta: 00:00:00.0000141
Sin test start (1500000 sin(x) calculations): 00:02:24.3446843
Sin test end (1500000 sin(x) calculations): 00:02:24.3446921
Sin test delta (1500000 sin(x) calculations): 00:00:00.0000078
Random typeof(double) array initialization and copy (1500000 elements) start: 00:02:24.3446921
Random typeof(double) array initialization and copy (1500000 elements) end: 00:02:24.3446984
Random typeof(double) array initialization and copy (1500000 elements) delta: 00:00:00.0000063

I test presentano dei risultati con variazioni poco apprezzabili tra una configurazione e l'altra.

Conclusioni


Con questi test abbiamo messo in evidenza due fattori importanti:

  1. A pari condizioni, il codice che fa uso di lambda genera un IL lievemente più grande.
  2. A pari condizioni, le prestazioni del codice che fa uso di lambda e quelle di codice che non ne fa uso sono pressoché identiche, a meno di ottimizzazioni e ritardi dello scheduler di esecuzioni dell'OS. Le differenze a favore delle lambda, infinitesimali nei nostri esempi, sono propenso a pensare siano dovuti maggiormente allo stato del gestore dei thread rispetto alle possibilità di ottimizzazione del codice. Tesi avvalorata dal fatto che a meno del delegate (che potrebbe permettere un precache del metodo?) e quindi del suo codice di allocazione e assegnazione il codice IL è pressoché identico.
Non ci resta che verificare i casi in cui andiamo a violare lo scope di variabile.

Nessun commento:

Posta un commento