我是Go和并发/并行编程的新手。为了试验goroutine(并希望看到它们的性能优势),我整理了一个小型测试程序,该程序简单地生成1亿个随机int
s -首先在单个goroutine中,然后在所报告的goroutine中使用runtime.NumCPU()
。
但是,与使用单个goroutine相比,我总是使用更多goroutine获得更差的性能。我认为我在程序设计或使用goroutines / channels /其他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) } }
icza.. 5
首先,让我们更正并优化代码中的某些内容:
从Go 1.5开始,GOMAXPROCS
默认设置为可用的CPU内核数,因此无需进行设置(尽管它没有害处)。
要生成的数字:
var numIntsToGenerate = 100000000 var numIntsPerThread = numIntsToGenerate / numThreads
如果numThreads
为3,则在使用多重goroutine的情况下,生成的数字会更少(由于整数除法),因此请更正它:
numIntsToGenerate = numIntsPerThread * numThreads
不需要1亿个值的缓冲区,请将其减小到合理的值(例如1000):
ch := make(chan int, 1000)
如果要使用append()
,则创建的切片应具有0的长度(和适当的容量):
singleThreadIntSlice := make([]int, 0, numIntsToGenerate) multiThreadIntSlice := make([]int, 0, numIntsToGenerate)
但是,在您的情况下这是不必要的,因为只有1个goroutine会收集结果,所以您可以简单地使用索引并创建如下切片:
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 }
好。代码现在更好了。尝试运行它时,您仍然会遇到多goroutine版本运行速度较慢的情况。这是为什么?
这是因为控制,同步和收集来自多个goroutine的结果确实有开销。如果他们执行的任务很少,则通信开销会更大,并且总体上您会失去性能。
你的情况就是这样。设置好后即可生成一个随机数,rand.Rand()
速度非常快。
让我们将“任务”修改为足够大,以便我们可以看到多个goroutine的好处:
// 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的计算时间超过多个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个块的形式传递随机数,则执行速度将提高2.5倍(与单个goroutine生成相比)。
最后一点:
您的单goroutine版本还使用了多个goroutine:1用于生成数字,而1用于收集结果。收集器很可能没有充分利用CPU内核,而只是等待结果,但仍然:使用2个CPU内核。让我们估计使用了“ 1.5”个CPU内核。而多goroutine版本使用4个CPU内核。粗略估算:4 / 1.5 = 2.66,非常接近我们的性能提升。
首先,让我们更正并优化代码中的某些内容:
从Go 1.5开始,GOMAXPROCS
默认设置为可用的CPU内核数,因此无需进行设置(尽管它没有害处)。
要生成的数字:
var numIntsToGenerate = 100000000 var numIntsPerThread = numIntsToGenerate / numThreads
如果numThreads
为3,则在使用多重goroutine的情况下,生成的数字会更少(由于整数除法),因此请更正它:
numIntsToGenerate = numIntsPerThread * numThreads
不需要1亿个值的缓冲区,请将其减小到合理的值(例如1000):
ch := make(chan int, 1000)
如果要使用append()
,则创建的切片应具有0的长度(和适当的容量):
singleThreadIntSlice := make([]int, 0, numIntsToGenerate) multiThreadIntSlice := make([]int, 0, numIntsToGenerate)
但是,在您的情况下这是不必要的,因为只有1个goroutine会收集结果,所以您可以简单地使用索引并创建如下切片:
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 }
好。代码现在更好了。尝试运行它时,您仍然会遇到多goroutine版本运行速度较慢的情况。这是为什么?
这是因为控制,同步和收集来自多个goroutine的结果确实有开销。如果他们执行的任务很少,则通信开销会更大,并且总体上您会失去性能。
你的情况就是这样。设置好后即可生成一个随机数,rand.Rand()
速度非常快。
让我们将“任务”修改为足够大,以便我们可以看到多个goroutine的好处:
// 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的计算时间超过多个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个块的形式传递随机数,则执行速度将提高2.5倍(与单个goroutine生成相比)。
最后一点:
您的单goroutine版本还使用了多个goroutine:1用于生成数字,而1用于收集结果。收集器很可能没有充分利用CPU内核,而只是等待结果,但仍然:使用2个CPU内核。让我们估计使用了“ 1.5”个CPU内核。而多goroutine版本使用4个CPU内核。粗略估算:4 / 1.5 = 2.66,非常接近我们的性能提升。