義理の兄弟がプレイしているビデオ ゲームに関連する私のちょっとした好奇心に答えるために、いじくり回してきた Go コードが少しあります。
基本的に、以下のコードは、ゲーム内のモンスターとの相互作用と、倒したときにモンスターがアイテムをドロップする頻度をシミュレートします。私が抱えている問題は、このようなコードが並列化に最適であると期待することですが、同時実行を追加すると、すべてのシミュレーションを実行するのにかかる時間が元の 4 ~ 6 倍遅くなる傾向があります。同時実行なし。
コードがどのように機能するかをよりよく理解できるように、3 つの主な機能があります。 プレイヤーとモンスターの間の単純な相互作用である相互作用機能。モンスターがアイテムをドロップする場合は 1、そうでない場合は 0 を返します。シミュレーション関数は、いくつかの相互作用を実行し、相互作用結果のスライスを返します (つまり、相互作用の成功/失敗を表す 1 と 0)。最後に、一連のシミュレーションを実行し、アイテムのドロップにつながったインタラクションの総数であるシミュレーション結果のスライスを返すテスト関数があります。これは、並行して実行しようとしている最後の関数です。
これで、実行したいテストごとにゴルーチンを作成すると、コードが遅くなる理由が理解できました。100 個のテストを実行していると仮定すると、私の MacBook Air に搭載されている 4 つの CPU にまたがるゴルーチンのそれぞれの間でコンテキストが切り替わると、パフォーマンスが低下しますが、プロセッサの数だけゴルーチンを作成し、テストの数をプロセッサ間で分割しています。ゴルーチン。各テストを並行して実行しているため、これにより実際にコードのパフォーマンスが向上すると予想されますが、もちろん、代わりに大幅な速度低下が発生しています。
なぜこれが起こっているのかを理解したいので、どんな助けでも大歓迎です。
以下は、go ルーチンのない通常のコードです。
package main
import (
"fmt"
"math/rand"
"time"
)
const (
NUMBER_OF_SIMULATIONS = 1000
NUMBER_OF_INTERACTIONS = 1000000
DROP_RATE = 0.0003
)
/**
* Simulates a single interaction with a monster
*
* Returns 1 if the monster dropped an item and 0 otherwise
*/
func interaction() int {
if rand.Float64() <= DROP_RATE {
return 1
}
return 0
}
/**
* Runs several interactions and retuns a slice representing the results
*/
func simulation(n int) []int {
interactions := make([]int, n)
for i := range interactions {
interactions[i] = interaction()
}
return interactions
}
/**
* Runs several simulations and returns the results
*/
func test(n int) []int {
simulations := make([]int, n)
for i := range simulations {
successes := 0
for _, v := range simulation(NUMBER_OF_INTERACTIONS) {
successes += v
}
simulations[i] = successes
}
return simulations
}
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println("Successful interactions: ", test(NUMBER_OF_SIMULATIONS))
}
そして、ゴルーチンとの並行コードは次のとおりです。
package main
import (
"fmt"
"math/rand"
"time"
"runtime"
)
const (
NUMBER_OF_SIMULATIONS = 1000
NUMBER_OF_INTERACTIONS = 1000000
DROP_RATE = 0.0003
)
/**
* Simulates a single interaction with a monster
*
* Returns 1 if the monster dropped an item and 0 otherwise
*/
func interaction() int {
if rand.Float64() <= DROP_RATE {
return 1
}
return 0
}
/**
* Runs several interactions and retuns a slice representing the results
*/
func simulation(n int) []int {
interactions := make([]int, n)
for i := range interactions {
interactions[i] = interaction()
}
return interactions
}
/**
* Runs several simulations and returns the results
*/
func test(n int, c chan []int) {
simulations := make([]int, n)
for i := range simulations {
for _, v := range simulation(NUMBER_OF_INTERACTIONS) {
simulations[i] += v
}
}
c <- simulations
}
func main() {
rand.Seed(time.Now().UnixNano())
nCPU := runtime.NumCPU()
runtime.GOMAXPROCS(nCPU)
fmt.Println("Number of CPUs: ", nCPU)
tests := make([]chan []int, nCPU)
for i := range tests {
c := make(chan []int)
go test(NUMBER_OF_SIMULATIONS/nCPU, c)
tests[i] = c
}
// Concatentate the test results
results := make([]int, NUMBER_OF_SIMULATIONS)
for i, c := range tests {
start := (NUMBER_OF_SIMULATIONS/nCPU) * i
stop := (NUMBER_OF_SIMULATIONS/nCPU) * (i+1)
copy(results[start:stop], <-c)
}
fmt.Println("Successful interactions: ", results)
}
更新 (01/12/13 18:05)
以下の「システム」の提案に従って、ゴルーチンごとに新しい Rand インスタンスを作成する、以下の並行コードの新しいバージョンを追加しました。コードのシリアル バージョンと比較して、非常にわずかに速度が向上しています (全体の所要時間が約 15 ~ 20% 短縮されています)。MBA の 4 つのコアにワークロードを分散させているため、時間が 75% 近く短縮されない理由を知りたいです。他に役立つ提案はありますか?
package main
import (
"fmt"
"math/rand"
"time"
"runtime"
)
const (
NUMBER_OF_SIMULATIONS = 1000
NUMBER_OF_INTERACTIONS = 1000000
DROP_RATE = 0.0003
)
/**
* Simulates a single interaction with a monster
*
* Returns 1 if the monster dropped an item and 0 otherwise
*/
func interaction(generator *rand.Rand) int {
if generator.Float64() <= DROP_RATE {
return 1
}
return 0
}
/**
* Runs several interactions and retuns a slice representing the results
*/
func simulation(n int, generator *rand.Rand) []int {
interactions := make([]int, n)
for i := range interactions {
interactions[i] = interaction(generator)
}
return interactions
}
/**
* Runs several simulations and returns the results
*/
func test(n int, c chan []int) {
source := rand.NewSource(time.Now().UnixNano())
generator := rand.New(source)
simulations := make([]int, n)
for i := range simulations {
for _, v := range simulation(NUMBER_OF_INTERACTIONS, generator) {
simulations[i] += v
}
}
c <- simulations
}
func main() {
rand.Seed(time.Now().UnixNano())
nCPU := runtime.NumCPU()
runtime.GOMAXPROCS(nCPU)
fmt.Println("Number of CPUs: ", nCPU)
tests := make([]chan []int, nCPU)
for i := range tests {
c := make(chan []int)
go test(NUMBER_OF_SIMULATIONS/nCPU, c)
tests[i] = c
}
// Concatentate the test results
results := make([]int, NUMBER_OF_SIMULATIONS)
for i, c := range tests {
start := (NUMBER_OF_SIMULATIONS/nCPU) * i
stop := (NUMBER_OF_SIMULATIONS/nCPU) * (i+1)
copy(results[start:stop], <-c)
}
fmt.Println("Successful interactions: ", results)
}
更新 (01/13/13 17:58)
私の問題を理解するのを手伝ってくれてありがとう。探していた答えがついに得られたので、同じ問題を抱えている人のためにここで要約すると思いました.
基本的に、私には 2 つの主な問題がありました。1 つ目は、私のコードは恥ずかしいほど並列でしたが、使用可能なプロセッサ間で分割すると実行速度が遅くなったことです。2 つ目は、別の問題が発生しました。これは、シリアル コードが 2 回実行されていたことですシングル プロセッサで実行されている同時実行コードと同じくらい遅く、ほぼ同じであると予想されます。どちらの場合も、問題は乱数ジェネレーター関数rand.Float64
でした。rand
基本的に、これはパッケージが提供する便利な機能です。そのパッケージでは、Rand
構造体のグローバル インスタンスが作成され、各コンビニエンス関数によって使用されます。このグローバルRand
インスタンスにはミューテックス ロックが関連付けられています。Rand
この便利な関数を使用していたため、グローバルインスタンスにアクセスするために各ゴルーチンを整列させる必要があったため、コードを実際に並列化することはできませんでした。Rand
解決策 (「システム」が以下に示すように) は、ゴルーチンごとに構造体の個別のインスタンスを作成することです。これにより、最初の問題は解決しましたが、2 つ目の問題が発生しました。
2 つ目の問題は、非並列並列コード (つまり、単一のプロセッサのみで実行される並列コード) が順次コードの 2 倍の速さで実行されることでした。この理由は、単一のプロセッサと単一のゴルーチンでしか実行していなかったにもかかわらず、そのゴルーチンには、Rand
私が作成した構造体の独自のインスタンスがあり、ミューテックス ロックなしで作成したためです。シーケンシャル コードはrand.Float64
、グローバル ミューテックスで保護されたRand
インスタンスを利用する便利な関数をまだ使用していました。そのロックを取得するためのコストが原因で、順次コードの実行速度が 2 倍遅くなりました。
したがって、この話の教訓は、パフォーマンスが重要な場合は常に、Rand
構造体のインスタンスを作成し、パッケージが提供する便利な関数を使用するのではなく、必要な関数を呼び出すようにすることです。