2017-01-13 21 views
1

Ich bin neu in Go und parallel/parallele Programmierung im Allgemeinen. Um die goroutines auszuprobieren (und hoffentlich die Leistungsvorteile zu sehen), habe ich ein kleines Testprogramm zusammengestellt, das einfach 100 Millionen zufällige int s erzeugt - zuerst in einer einzigen goroutine und dann in so vielen goroutines wie von runtime.NumCPU() gemeldet .Generieren von Zufallszahlen gleichzeitig in Go

Allerdings bekomme ich immer schlechtere Leistung mit mehr Göroutinen als mit einem einzigen. Ich vermute, dass mir etwas Wichtiges fehlt, entweder in meinem Programmdesign oder in der Art und Weise, wie ich Goroutinen/Kanäle/andere Go-Funktionen nutze. Jede Rückmeldung wird sehr geschätzt.

Ich füge den Code unten an.

package main 

import "fmt" 
import "time" 
import "math/rand" 
import "runtime" 

func main() { 
    // Figure out how many CPUs are available and tell Go to use all of them 
    numThreads := runtime.NumCPU() 
    runtime.GOMAXPROCS(numThreads) 

    // Number of random ints to generate 
    var numIntsToGenerate = 100000000 
    // Number of ints to be generated by each spawned goroutine thread 
    var numIntsPerThread = numIntsToGenerate/numThreads 
    // Channel for communicating from goroutines back to main function 
    ch := make(chan int, numIntsToGenerate) 

    // Slices to keep resulting ints 
    singleThreadIntSlice := make([]int, numIntsToGenerate, numIntsToGenerate) 
    multiThreadIntSlice := make([]int, numIntsToGenerate, numIntsToGenerate) 

    fmt.Printf("Initiating single-threaded random number generation.\n") 
    startSingleRun := time.Now() 
    // Generate all of the ints from a single goroutine, retrieve the expected 
    // number of ints from the channel and put in target slice 
    go makeRandomNumbers(numIntsToGenerate, ch) 
    for i := 0; i < numIntsToGenerate; i++ { 
    singleThreadIntSlice = append(singleThreadIntSlice,(<-ch)) 
    } 
    elapsedSingleRun := time.Since(startSingleRun) 
    fmt.Printf("Single-threaded run took %s\n", elapsedSingleRun) 


    fmt.Printf("Initiating multi-threaded random number generation.\n") 
    startMultiRun := time.Now() 
    // Run the designated number of goroutines, each of which generates its 
    // expected share of the total random ints, retrieve the expected number 
    // of ints from the channel and put in target slice 
    for i := 0; i < numThreads; i++ { 
    go makeRandomNumbers(numIntsPerThread, ch) 
    } 
    for i := 0; i < numIntsToGenerate; i++ { 
    multiThreadIntSlice = append(multiThreadIntSlice,(<-ch)) 
    } 
    elapsedMultiRun := time.Since(startMultiRun) 
    fmt.Printf("Multi-threaded run took %s\n", elapsedMultiRun) 
} 


func makeRandomNumbers(numInts int, ch chan int) { 
    source := rand.NewSource(time.Now().UnixNano()) 
    generator := rand.New(source) 
    for i := 0; i < numInts; i++ { 
     ch <- generator.Intn(numInts*100) 
    } 
} 

Antwort

4

Lassen Sie uns zunächst richtig und einige Dinge in Ihrem Code optimieren:

Seit Go 1.5, GOMAXPROCS standardmäßig die Anzahl der CPU-Kerne zur Verfügung, so dass keine Notwendigkeit, dass zu setzen (obwohl es nicht schadet).

Zahlen zu generieren:

var numIntsToGenerate = 100000000 
var numIntsPerThread = numIntsToGenerate/numThreads 

Wenn numThreads wie 3, im Falle von Multi goroutines ist, werden Sie weniger Zahlen erzeugt haben (aufgrund Division integer), so machen wir es korrigieren:

numIntsToGenerate = numIntsPerThread * numThreads 

keine Notwendigkeit, einen Puffer für 100 Millionen Werte, dass (zB 1000) zu einem vernünftigen Wert verringern:

ch := make(chan int, 1000) 

Wenn Sie append() verwenden, die Scheiben sollten Sie 0 Länge (und richtige Kapazität) haben zu erstellen:

singleThreadIntSlice := make([]int, 0, numIntsToGenerate) 
multiThreadIntSlice := make([]int, 0, numIntsToGenerate) 

Aber in Ihrem Fall, die unnötig ist, da nur 1 goroutine die Ergebnisse sammelt, können Sie einfach Indizierung und erstellen Scheiben wie folgt aus:

singleThreadIntSlice := make([]int, numIntsToGenerate) 
multiThreadIntSlice := make([]int, numIntsToGenerate) 

und wenn das Sammeln Ergebnisse:

for i := 0; i < numIntsToGenerate; i++ { 
    singleThreadIntSlice[i] = <-ch 
} 

// ... 

for i := 0; i < numIntsToGenerate; i++ { 
    multiThreadIntSlice[i] = <-ch 
} 

Ok. Der Code ist jetzt besser. Wenn Sie versuchen, es auszuführen, werden Sie immer noch feststellen, dass die Multi-Gorroutine-Version langsamer ausgeführt wird. Warum das?

Das liegt daran, dass das Steuern, Synchronisieren und Sammeln von Ergebnissen aus mehreren Gououtines einen Overhead hat. Wenn die von ihnen ausgeführte Aufgabe gering ist, wird der Kommunikationsaufwand größer und Sie verlieren insgesamt an Leistung.

Ihr Fall ist so ein Fall. Die Erstellung einer einzelnen Zufallszahl, sobald Sie Ihre rand.Rand() einrichten, ist ziemlich schnell.

Lassen Sie uns Ihre „Aufgabe“ ändern groß genug zu sein, damit wir den Nutzen mehrerer goroutines sehen:

// 1 million is enough now: 
var numIntsToGenerate = 1000 * 1000 


func makeRandomNumbers(numInts int, ch chan int) { 
    source := rand.NewSource(time.Now().UnixNano()) 
    generator := rand.New(source) 
    for i := 0; i < numInts; i++ { 
     // Kill time, do some processing: 
     for j := 0; j < 1000; j++ { 
      generator.Intn(numInts * 100) 
     } 
     // and now return a single random number 
     ch <- generator.Intn(numInts * 100) 
    } 
} 

In diesem Fall eine Zufallszahl zu erhalten, erzeugen wir 1000 Zufallszahlen und sie nur werfen weg (um eine Rechen-/Tötungszeit zu machen), bevor wir die eine erzeugen, die wir zurückgeben.Wir machen das so, dass die Berechnungszeit der Arbeiter den Kommunikationsoverhead von mehreren goroutines ausmacht.

Ausführen der App jetzt, meine Ergebnisse auf einer 4-Core-Maschine:

Initiating single-threaded random number generation. 
Single-threaded run took 2.440604504s 
Initiating multi-threaded random number generation. 
Multi-threaded run took 987.946758ms 

Die Multi-goroutine Version läuft 2,5 mal schneller. Das heißt, wenn Ihre Goroutinen Zufallszahlen in 1000-Blöcken liefern würden, würden Sie eine 2,5-mal schnellere Ausführung sehen (verglichen mit der Einzel-Goroutinen-Generation).

Eine letzte Anmerkung:

Ihre Single-goroutine Version verwendet auch mehrere goroutines: 1 Zahlen zu erzeugen und 1 die Ergebnisse zu sammeln. Wahrscheinlich verwendet der Kollektor einen CPU-Kern nicht vollständig und wartet meistens nur auf die Ergebnisse, aber dennoch: 2 CPU-Kerne werden verwendet. Lassen Sie uns schätzen, dass "1,5" CPU-Kerne verwendet werden. Während die Multi-Gorroutine-Version 4 CPU-Kerne verwendet. Nur als grobe Schätzung: 4/1,5 = 2,66, sehr nahe an unserem Leistungsgewinn.

+1

Okay, das macht alles sehr sinnvoll. Ich vermutete, dass dies mit den Kosten verbunden sein könnte, die mit der Nutzung des Kanals verbunden sind (obwohl ich nicht in Bezug auf den Kommunikationsaufwand in Bezug auf die Arbeitseinheit denke, sondern eher in Bezug auf mögliche Überlastung des Kanals) oder etwas ähnliches). Vielen Dank für die klare und pädagogische Lösung! – Karl

0

Wenn Sie die Zufallszahlen wirklich parallel erzeugen möchten, sollte jede Aufgabe darin bestehen, die Zahlen zu generieren und sie dann in einem Schritt zurückzugeben, anstatt dass die Aufgabe jeweils eine Zahl erzeugt und sie einem Kanal zuführt Lesen und Schreiben in den Kanal wird die Dinge im Multi-Go-Routine-Fall verlangsamen. Unten ist der modifizierte Code, wo die Aufgabe die benötigten Zahlen auf einmal generiert, und dies funktioniert besser in Multi-Go-Routinen. Außerdem habe ich Slice von Slices verwendet, um das Ergebnis von Multi-Go-Routinen zu sammeln.

package main 

import "fmt" 
import "time" 
import "math/rand" 
import "runtime" 

func main() { 
    // Figure out how many CPUs are available and tell Go to use all of them 
    numThreads := runtime.NumCPU() 
    runtime.GOMAXPROCS(numThreads) 

    // Number of random ints to generate 
    var numIntsToGenerate = 100000000 
    // Number of ints to be generated by each spawned goroutine thread 
    var numIntsPerThread = numIntsToGenerate/numThreads 

    // Channel for communicating from goroutines back to main function 
    ch := make(chan []int) 

    fmt.Printf("Initiating single-threaded random number generation.\n") 
    startSingleRun := time.Now() 
    // Generate all of the ints from a single goroutine, retrieve the expected 
    // number of ints from the channel and put in target slice 
    go makeRandomNumbers(numIntsToGenerate, ch) 
    singleThreadIntSlice := <-ch 
    elapsedSingleRun := time.Since(startSingleRun) 
    fmt.Printf("Single-threaded run took %s\n", elapsedSingleRun) 

    fmt.Printf("Initiating multi-threaded random number generation.\n") 

    multiThreadIntSlice := make([][]int, numThreads) 
    startMultiRun := time.Now() 
    // Run the designated number of goroutines, each of which generates its 
    // expected share of the total random ints, retrieve the expected number 
    // of ints from the channel and put in target slice 
    for i := 0; i < numThreads; i++ { 
     go makeRandomNumbers(numIntsPerThread, ch) 
    } 
    for i := 0; i < numThreads; i++ { 
     multiThreadIntSlice[i] = <-ch 
    } 
    elapsedMultiRun := time.Since(startMultiRun) 
    fmt.Printf("Multi-threaded run took %s\n", elapsedMultiRun) 
    //To avoid not used warning 
    fmt.Print(len(singleThreadIntSlice)) 
} 

func makeRandomNumbers(numInts int, ch chan []int) { 
    source := rand.NewSource(time.Now().UnixNano()) 
    generator := rand.New(source) 
    result := make([]int, numInts) 
    for i := 0; i < numInts; i++ { 
     result[i] = generator.Intn(numInts * 100) 
    } 
    ch <- result 
} 
+0

Ich würde "multidimensional slice" durch "slice of slices" ersetzen: Go hat keine mehrdimensionalen Slices, also lassen Sie uns nicht so tun, als existierten sie ;-) – kostix

+0

@kostix Sure Sache, aktualisiert :) – Ankur

Verwandte Themen