Quali scenari utilizzare le raccolte simultanee .NET in modo efficace?

3

Sto studiando come introdurre il parallelismo in un'applicazione per migliorare le prestazioni. In particolare, sto analizzando parallelamente i loop e le loro varianti e i miei esperimenti iniziali mostrano un sovraccarico significativo nell'utilizzo di raccolte dal namespace System.Collections.Concurrent .

Scenario tipico

Vuoi eseguire un'iterazione su una raccolta in parallelo, eseguire alcuni calcoli per iterazione e quindi indirizzare / aggiungere i risultati in una raccolta appropriata in base ai dettagli dell'iterazione.

[Se non utilizzi le Concurrent Collections ottieni errori dovuti al solito multi-threading & problemi di sincronizzazione, quindi sembra che tu sia obbligato a utilizzare le raccolte simultanee.]

Sfortunatamente i miei test per lo più / spesso dimostrano che le soluzioni semplici a thread singolo sono più veloci a causa del sovraccarico dell'utilizzo delle Concurrent Collections - è prevedibile?

Conclusioni sperimentali

Dovresti eseguire una "grande" parte del lavoro per iterazione affinché le raccolte simultanee funzionino meglio di una singola soluzione con thread?

Esempio di codice

Come da richieste nei commenti:

open System.Collections.Generic
open System.Collections.Concurrent
open FSharp.Collections.ParallelSeq

type Data = {
    Name:string
    mutable Age:int
    Description:string
}

[<EntryPoint>]
let main argv = 

    let xs = ResizeArray<Data>()
    let n = 1000000

    let rand = System.Random()

    // create some random data
    for i in 0..n do
        let f1 = rand.NextDouble()
        let f2 = rand.NextDouble()
        let data = {Name = "My Name" + f1.ToString() + f2.ToString(); Age = 38; Description = "Happy"}
        xs.Add(data)

    // single-threaded example
    let normalCollection = Dictionary<string,Data>()
    let stopWatch = System.Diagnostics.Stopwatch()
    stopWatch.Start()
    xs |> Seq.iter (fun x -> 
        x.Age <- x.Age + rand.Next()
        normalCollection.[x.Name] <- x
        )
    stopWatch.Stop()
    printfn "Single Threaded: %A" stopWatch.Elapsed

    System.GC.Collect(2)

    // single-threaded + concurrent Collection example
    let concurrentCollection = ConcurrentDictionary<string,Data>()
    let stopWatch = System.Diagnostics.Stopwatch()
    stopWatch.Start()
    xs |> Seq.iter (fun x -> 
        x.Age <- x.Age + rand.Next()
        concurrentCollection.[x.Name] <- x
        )
    stopWatch.Stop()
    printfn "Single Threaded + Concurrent Collection: %A" stopWatch.Elapsed

    concurrentCollection.Clear()

    System.GC.Collect(2)

    // multi-threaded example
    let concurrentCollection = ConcurrentDictionary<string,Data>()
    let stopWatch = System.Diagnostics.Stopwatch()
    stopWatch.Start()
    xs |> PSeq.iter (fun x -> 
        x.Age <- x.Age + rand.Next()
        concurrentCollection.[x.Name] <- x
        )
    stopWatch.Stop()
    printfn "Multi-Threaded: %A" stopWatch.Elapsed

    // Functional Style
    normalCollection.Clear()
    System.GC.Collect(2)
    let stopWatch = System.Diagnostics.Stopwatch()
    stopWatch.Start()
    let ys = 
        xs 
        |> PSeq.map (fun x -> {x with Age = x.Age + rand.Next()} )
        |> Seq.iter (fun x -> normalCollection.[x.Name] <- x)
    stopWatch.Stop()
    printfn "Functional Style: %A" stopWatch.Elapsed

    printfn "%A" argv
    0 // return an integer exit code

Timing

Costante su più esecuzioni ...

n = 10.000 (set di dati di piccole dimensioni)

Single Threaded: 00:00:00.0048328

Single Threaded + Concurrent Collection: 00:00:00.0053433

Multi-Threaded: 00:00:00.0197919

Functional Style: 00:00:00.0073613

n = 10.000.000 (set di dati di grandi dimensioni)

Single Threaded: 00:00:04.0683213

Single Threaded + Concurrent Collection: 00:00:13.6469918

Multi-Threaded: 00:00:10.9768615

Functional Style: 00:00:04.6633076

    
posta Sam 21.10.2015 - 13:23
fonte

3 risposte

6

Ti manca il punto. Le collezioni concorrenti non sono così tanto per le prestazioni. Sono lì quindi non è necessario provare a fare tutto il blocco attorno a un dizionario / fare la coda, dato che farlo è incline agli errori e noioso. (E sono davvero lì, quindi non è necessario provare a implementare dizionari / code lockless da soli)

Usarli in modo efficace è semplice come "hey, ho una coda / dizionario / etc, e deve essere accessibile contemporaneamente. Invece di reinventare la ruota, userò queste classi pre-costruite". Non li usi per introdurre la concorrenza, tu introduci la concorrenza perché ne hai bisogno e queste cose aiutano.

    
risposta data 21.10.2015 - 15:52
fonte
1

Quello che stai misurando è la velocità di aggiungere una coppia fissa valore-chiave a un Dictionary nella parte di riduzione di un'operazione di riduzione della mappa.

Il ConcurrentDictionary è inteso per un caso d'uso più generale: lettori e scrittori concorrenti, nessun coordinamento tra i partecipanti, scrive sulle stesse chiavi, legge le stesse chiavi. C'è un costo in termini di prestazioni per ciascuna di queste funzioni, quindi non sorprende che ConcurrentDictionary non sia competitivo nel tuo benchmark.

Due modi per esplorare da qui potrebbero essere:

  1. Come implementare un Dictionary che guadagna da più thread nel tuo benchmark?
  2. Che cos'è un benchmark realistico che richiede le funzioni ConcurrentDictionary e in che modo il confronto delle prestazioni è in questo caso?
risposta data 21.10.2015 - 22:42
fonte
1

You would need to be doing a "very large" piece of work per iteration for the Concurrent Collections to perform better than a single threaded solution?

Si potrebbe dire che, fintanto che "molto grande" significa qualcosa come "operazione che richiede più di un ordine di grandezza più di un'operazione di raccolta simultanea". Ma poiché le operazioni di raccolta simultanee sono ancora piuttosto veloci, il tuo "molto grande" in realtà non è affatto grande.

Le raccolte concorrenti non sono realmente destinate a competere con le collezioni standard, ma sono in concorrenza con locking , che è ciò che si farebbe normalmente quando si volesse modificare la stessa collezione da più thread. E rispetto al blocco, le raccolte concorrenti sono più efficienti (e anche più facili da usare correttamente).

Ma la parallelizzazione o le raccolte simultanee non sono una pallottola d'argento, non renderanno il codice che non può essere parallelizzato magicamente più velocemente.

Ci sono anche alcuni problemi con il tuo benchmark, che possono influenzare i risultati:

  1. Random non è thread-safe, non dovresti usarlo da più thread.
  2. Stai riutilizzando la stessa raccolta nei test successivi. Questo è problematico, perché significa che solo la prima misura include il ridimensionamento della collezione.
  3. Il test "Stile funzionale" genera molti rifiuti che probabilmente non saranno raccolti entro la fine del test. Per questo motivo, preferisco chiamare GC.Collect() prima di arrestando il cronometro. In questo modo, il codice che genera molti rifiuti è adeguatamente penalizzato.
risposta data 04.11.2015 - 18:17
fonte

Leggi altre domande sui tag