2017-01-13 4 views
1

저는 일반적으로 동시/병렬 프로그래밍에 익숙합니다. goroutines을 시험해보기 위해 (그리고 잘하면 성능상의 이점을 확인하기 위해), 나는 단일 goroutine에서 처음으로 1 억 개의 무작위 int을 생성하는 작은 테스트 프로그램을 작성한 다음 runtime.NumCPU()에 의해보고 된 많은 goroutines .Go에서 동시 발생 난수 생성

그러나 하나의 goroutines를 사용하는 것보다 더 많은 goroutines를 사용하여 지속적으로 성능이 저하됩니다. 내 프로그램 디자인이나 goroutines/채널/다른 Go 기능을 사용하는 방식에서 중요한 무엇인가를 놓치고 있다고 가정합니다. 모든 의견을 많이 주시면 감사하겠습니다.

아래 코드를 첨부합니다.

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) 
    } 
} 

답변

4

먼저하자 정확하고 코드에서 몇 가지 최적화 :

이동 1.5부터 사용 가능한 CPU 코어의 수, 그래서 (이 전혀 해를 끼치 지 않는 없지만) 그 설정할 필요에 GOMAXPROCS 기본 설정됩니다. numThreads 멀티 goroutines의 경우, 3과 같다

var numIntsToGenerate = 100000000 
var numIntsPerThread = numIntsToGenerate/numThreads 

경우 (부문을 정수로 인해) 발생 덜 수있을 것이다, 그래서 그것을 해결하자 :

숫자는 생성

numIntsToGenerate = numIntsPerThread * numThreads 

ch := make(chan int, 1000) 
:

없음 합리적인 값 것을 감소 ​​(예 : 1000) 값이 1 억위한 버퍼를 필요 691,363,210

당신은 append(), 당신은 0 길이 (적절한 용량)이 있어야 만든 조각 사용하려면 : 만 1 goroutine 결과를 수집, 당신은 간단하게 사용할 수있는, 불필요 귀하의 경우

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

을 이 같은 색인, 만들 조각 :

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

그리고 수집 결과 :

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

// ... 

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

좋아. 코드가 더 좋습니다. 실행하려고 시도하면 멀티 고 루틴 버전이 더 느리게 실행되는 것을 경험하게됩니다. 왜 그런가요?

여러 goroutines의 결과를 제어, 동기화 및 수집하기 때문에 오버 헤드가 발생하기 때문입니다. 그들이 수행하는 작업이 적 으면 통신 오버 헤드가 더 커지고 전반적으로 성능이 저하됩니다.

귀하의 사례는 그런 경우입니다. rand.Rand()을 설정 한 후 단일 임의 번호 생성은 매우 빠릅니다.

우리가 여러 goroutines의 혜택을 볼 수 있도록의 충분한 크기로 당신의 "작업"을 수정할 수 : 임의의 숫자를 얻기 위해이 경우

// 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) 
    } 
} 

을, 우리는 1000 난수를 생성하고 그냥 던져 우리가 반환하는 것을 생성하기 전에 (계산/죽이는 시간을 만들기 위해).우리는 이렇게하여 작업자의 계산 시간이 , 초과 값이 인 여러 goroutine의 통신 오버 헤드를 계산합니다.

, 이제 응용 프로그램을 실행 4 코어 시스템에서 내 결과 :

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

멀티 goroutine 버전은 빠른 2.5 배를 실행합니다. 즉, goroutine이 1000 블록으로 임의의 숫자를 전달할 경우 단일 goroutine 생성과 비교하여 2.5 배 빠른 실행을 보게됩니다.

마지막 주 : 숫자를 생성하기 위해 1 일 결과를 수집 :

귀하의 단일 goroutine 버전은 여러 goroutines를 사용합니다. 대부분의 수집가는 CPU 코어를 완전히 활용하지 않고 대부분 결과를 기다리지 만 여전히 2 개의 CPU 코어가 사용됩니다. "1.5"CPU 코어가 사용된다고 추정 해 봅시다. 멀티 고 루틴 버전은 4 개의 CPU 코어를 사용합니다. 대략적인 추정치와 마찬가지로 4/1.5 = 2.66으로 성능 향상에 매우 가깝습니다.

+1

좋아요, 모든 것이 합리적입니다. 나는 그것이 채널 사용과 관련된 비용과 관련이있을 것이라고 추측했다. (나는 작업 단위와 관련된 통신 오버 헤드에 관해서는 생각하지 않았지만 오히려 채널의 혼잡 가능성에 관해서 생각했다. 또는 그런 것). 명확하고 교육적인 연습에 많은 감사드립니다! – Karl

0

난수를 병렬로 생성하려면 각 작업이 숫자를 생성 한 다음 한 번에 하나의 숫자를 생성하는 작업이 아닌 하나의 작업으로 반환해야합니다. 채널을 읽고 쓰는 것은 여러 번 일상적인 상황에서 상황을 늦출 수 있습니다. 아래는 수정 된 코드입니다. 그런 다음 작업이 필요한 숫자를 한 번에 생성합니다.이 방법은 멀티 루틴의 경우 더욱 잘 수행되며 멀티 슬라이스에서 결과를 수집하기 위해 슬라이스 조각을 사용했습니다.

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

"다차원 슬라이스"를 "조각 슬라이스"로 바꿉니다. 이동에는 다차원 슬라이스가 없으므로 존재하지 않는 것입니다 .-) – kostix

+0

@kostix – Ankur