2016-03-03 27 views
11

FWIW Penso che i problemi descritti qui si riducano al fatto che il compilatore C# è più intelligente e rende efficiente una macchina a stati modello per gestire il codice asincrono, mentre il compilatore F # crea una miriade di oggetti e chiamate di funzione che sono generalmente meno efficienti.Prestazioni di asincrona in F # rispetto a C# (esiste un modo migliore per scrivere asincrona {...})

Comunque, se ho la funzione C# di seguito:

public async static Task<IReadOnlyList<T>> CSharpAsyncRead<T>(
     SqlCommand cmd, 
     Func<SqlDataReader, T> createDatum) 
{ 
    var result = new List<T>(); 
    var reader = await cmd.ExecuteReaderAsync(); 

    while (await reader.ReadAsync()) 
    { 
     var datum = createDatum(reader); 
     result.Add(datum); 
    } 

    return result.AsReadOnly(); 
} 

E poi convertire questo a F # come segue:

let fsharpAsyncRead1 (cmd:SqlCommand) createDatum = async { 
    let! reader = 
     Async.AwaitTask (cmd.ExecuteReaderAsync()) 

    let rec readRows (results:ResizeArray<_>) = async { 
     let! readAsyncResult = Async.AwaitTask (reader.ReadAsync()) 
     if readAsyncResult then 
      let datum = createDatum reader 
      results.Add datum 
      return! readRows results 
     else 
      return results.AsReadOnly() :> IReadOnlyList<_> 
    } 

    return! readRows (ResizeArray()) 
} 

poi scopro che le prestazioni del codice F # è notevolmente più lento e più affamati di CPU, rispetto alla versione C#. Mi stavo chiedendo se fosse meglio comporlo. Ho provato a rimuovere la funzione ricorsiva come segue (che è apparso un po 'brutto con il no po' e non lasciare mutevole s!):

let fsharpAsyncRead2 (cmd:SqlCommand) createDatum = async { 
    let result = ResizeArray() 

    let! reader = 
     Async.AwaitTask (cmd.ExecuteReaderAsync()) 

    let! moreData = Async.AwaitTask (reader.ReadAsync()) 
    let mutable isMoreData = moreData 
    while isMoreData do 
     let datum = createDatum reader 

     result.Add datum 

     let! moreData = Async.AwaitTask (reader.ReadAsync()) 
     isMoreData <- moreData 

    return result.AsReadOnly() :> IReadOnlyList<_> 
} 

Ma la prestazione è stata fondamentalmente la stessa.

Come esempio delle prestazioni, quando stavo caricando una barra dei dati di mercato, quali:

type OHLC = { 
    Time : DateTime 
    Open : float 
    High : float 
    Low : float 
    Close : float 
} 

Sulla mia macchina, la versione # asincrona F preso ~ il doppio del tempo, e consumò ~ due volte tanto Risorse della CPU per tutto il tempo in cui è stata eseguita, quindi circa 4 volte il numero di risorse (vale a dire internamente che deve generare più thread?).

(Forse è un po 'discutibile fare una lettura di una struttura così banale? Sto solo spiando la macchina per vedere cosa fa. Rispetto alla versione non asincrona C# uno completa in ~ stesso tempo, ma consuma> il doppio della CPU, cioè il diritto Read() consuma < 1/8 delle risorse f #)

Quindi la mia domanda è, mentre sto facendo il F # async il "giusto" modo (questo è stato il mio primo tentativo di utilizzo)?

(... e se io sono, poi fare ho solo bisogno di andare a modificare il compilatore per aggiungere una macchina a stati implementazione basata per Asyncs compilati ... quanto sia difficile potrebbe essere che :-))

+0

Sei sicuro che entrambi corrano con le stesse impostazioni di testimone, ottimizzazione e così via? –

+3

Hai guardato 'hopac' (https://github.com/Hopac/Hopac)? ha un sovraccarico significativo della CPU inferiore rispetto a 'Async' (e IIRC inferiore a' Task'). FYI; Personalmente non considererei l'uso di 'Async' o' TPL' e così via quando ho bisogno del parallelismo. Se sono legato alla CPU, molto probabilmente non posso supportare le astrazioni. – FuleSnabel

+0

@FuleSnabel BTW, ho preso in considerazione Hopac perché è effettivamente più veloce di 'async {}'. Ma l'interoperabilità con C# (mancanza di) era un dealbreaker. L'espressione di calcolo 'task {}' offre un modo per usare TPL (che è probabilmente il pezzo più ottimizzato di .NET) nel modo idiomatico F #. –

risposta

11

Il numero Async di F # e il limite TPL (Async.AwaitTask/Async.StartAsTask) è la cosa più lenta. Ma in generale, F # Async è più lento di per sé e dovrebbe essere usato per l'IO bound non per le attività legate alla CPU. È possibile trovare questo repo interessante: https://github.com/buybackoff/FSharpAsyncVsTPL

Fondamentalmente, ho confrontato i due e anche un'espressione di calcolo del task build, che è originariamente dal progetto FSharpx. Task Builder è molto più veloce se utilizzato insieme a TPL. Uso questo approccio nella mia libreria Spreads, che è scritta in F # ma si basa su TPL. Il this line è un binding altamente ottimizzato dell'espressione di computazione che effettivamente fa la stessa cosa di asincrona/attesa di C# dietro le quinte. Ho confrontato ogni uso dell'espressione di calcolo task{} nella libreria ed è molto veloce (il gotcha non deve essere usato per/while dell'espressione di calcolo, ma ricorsione). Inoltre, rende il codice interoperabile con C#, mentre async di F # non può essere consumato da C#.

+0

Non sono riuscito a vedere alcuna prestazione diversa con GetAwaiter(). Ecco un elenco dei miei test - https://gist.github.com/manofstick/e0f1cbf14febf3666c06 - Ho provato 32/64-bit, con e senza gcServer. (e ho controllato che il percorso veloce m.IsCompleted non venisse colpito continuamente) –

+0

... e in realtà se modifichi quell'elenco per aumentare il numero di attività create per essere abbastanza grandi (milioni), abbassa il loro caricare il lavoro in modo che siano abbastanza veloci, creare i task sospesi e quindi solo start() li prima del bind, quindi il ContinueWith supera la versione GetAwaiter. –

+0

@PaulWestcott Quando ho provato la differenza non era grande e rientrava nel margine di errore. L'utilizzo di AsyncTaskMethodBuilder invece di TaskCompletionSource era in realtà leggermente più lento (un paio di percentuali), ma ho deciso di non allocare oggetti aggiuntivi se possibile. –

Problemi correlati