2012-05-04 15 views
7

Codice esempio:F # "per il ciclo" ottimizzazione

let foo1 (arr : int[]) = 
    for i = 0 to arr.Length-1 do 
     arr.[i] <- i 

let foo2 (arr : int[]) = 
    for i in [0..arr.Length-1] do 
     arr.[i] <- i 

ho pensato che questo funzioni dovrebbero essere equivalenti tra loro (in termini di prestazioni). Ma se guardiamo IL profilo, vedremo:

prima funzione 15 linee, non allocazioni dinamiche, senza try operatore, nessuna chiamata virtuale:

IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: stloc.0 
IL_0003: br.s IL_0011 
// loop start (head: IL_0011) 
    IL_0005: ldarg.0 
    IL_0006: ldloc.0 
    IL_0007: ldloc.0 
    IL_0008: stelem.any [mscorlib]System.Int32 
    IL_000d: ldloc.0 
    IL_000e: ldc.i4.1 
    IL_000f: add 
    IL_0010: stloc.0 

    IL_0011: ldloc.0 
    IL_0012: ldarg.0 
    IL_0013: ldlen 
    IL_0014: conv.i4 
    IL_0015: blt.s IL_0005 
// end loop 

IL_0017: ret 

e un secondo - quasi 100 linee, un sacco di assegnazioni/deallocazioni, le chiamate di funzioni virtuali, un sacco di try/Dispose:

IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: ldc.i4.1 
IL_0003: ldarg.0 
IL_0004: ldlen 
IL_0005: conv.i4 
IL_0006: ldc.i4.1 
IL_0007: sub 
IL_0008: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [FSharp.Core]Microsoft.FSharp.Core.Operators/OperatorIntrinsics::RangeInt32(int32, int32, int32) 
IL_000d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [FSharp.Core]Microsoft.FSharp.Core.Operators::CreateSequence<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>) 
IL_0012: call class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<!!0> [FSharp.Core]Microsoft.FSharp.Collections.SeqModule::ToList<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>) 
IL_0017: stloc.0 
IL_0018: ldloc.0 
IL_0019: unbox.any class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> 
IL_001e: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() 
IL_0023: stloc.1 
.try 
{ 
    // loop start (head: IL_0024) 
     IL_0024: ldloc.1 
     IL_0025: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() 
     IL_002a: brfalse.s IL_003e 

     IL_002c: ldloc.1 
     IL_002d: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current() 
     IL_0032: stloc.3 
     IL_0033: ldarg.0 
     IL_0034: ldloc.3 
     IL_0035: ldloc.3 
     IL_0036: stelem.any [mscorlib]System.Int32 
     IL_003b: nop 
     IL_003c: br.s IL_0024 
    // end loop 

    IL_003e: ldnull 
    IL_003f: stloc.2 
    IL_0040: leave.s IL_005b 
} // end .try 
finally 
{ 
    IL_0042: ldloc.1 
    IL_0043: isinst [mscorlib]System.IDisposable 
    IL_0048: stloc.s 4 
    IL_004a: ldloc.s 4 
    IL_004c: brfalse.s IL_0058 

    IL_004e: ldloc.s 4 
    IL_0050: callvirt instance void [mscorlib]System.IDisposable::Dispose() 
    IL_0055: ldnull 
    IL_0056: pop 
    IL_0057: endfinally 

    IL_0058: ldnull 
    IL_0059: pop 
    IL_005a: endfinally 
} // end handler 

IL_005b: ldloc.2 
IL_005c: pop 
IL_005d: ret 

mia domanda è perché fa F # compilatore utilizza codice in modo complicato per foo2? Perché usa un IEnumerable per implementare un ciclo così insignificante?

risposta

12

Nel 2 ° esempio se si utilizza l'espressione gamma, che sarà convertito in normale ciclo for:

let foo2 (arr : int[]) = 
    for i in 0..arr.Length-1 do 
     arr.[i] <- i 

e diventare equivalente a foo1.

cito Section 6.3.12 Range Expressions in F# language specs:

Una sequenza di iterazione espressione della forma per var in espr1 espr2 .. do expr3 fatto a volte è elaborato come un semplice ciclo for-espressione (§6.5.7).

Tuttavia, il secondo esempio è più simile a:

let foo2 (arr : int[]) = 
    let xs = [0..arr.Length-1] (* A new list is created *) 
    for i in xs do 
     arr.[i] <- i 

cui è stato creato un nuovo elenco in modo esplicito.

+0

Non conoscevo questa espressione 'for i in 0..arr.Length-1 do'. Molte grazie. – qehgt

7

Ciò che viene visualizzato è la differenza standard tra l'utilizzo di un'enumerazione basata su indice e l'enumerazione basata su IEnumerable (o in termini C# for rispetto a foreach).

Nel secondo campione l'espressione [0..arr.Length-1] sta creando una raccolta e F # utilizza IEnumerable<T> per enumerare i valori. Parte di questo stile di enumerazione implica l'uso di IEnumerator<T> che implementa IDisposable. Il blocco try/finally che viene visualizzato viene generato per garantire che il metodo IDisposable::Dispose venga chiamato alla fine dell'enumerazione anche in presenza di un'eccezione.

Potrebbe F # ottimizzare il secondo esempio nel primo ed evitare tutto il sovraccarico? È possibile che potrebbero fare questa ottimizzazione. Sbircia essenzialmente attraverso l'espressione dell'intervallo, non è solo un semplice intervallo numerico e quindi genera il codice for equivalente.

Se F # ottimizza il secondo esempio. Il mio voto sarebbe no. Funzionalità come questa spesso sembrano banali dall'esterno, ma in realtà implementarle e, cosa più importante, mantenerle, possono essere piuttosto costose. Un utente astuto può sempre convertire il suo codice nella versione standard for ed evitare l'overhead IEnumerable<T> (se il profiler dovesse rivelare un problema).Non implementare l'ottimizzazione libera il team di F # per implementare altre fantastiche funzionalità.

Problemi correlati