У меня есть код Go, который я возился, чтобы ответить на небольшое любопытство, связанное с видеоигрой, которую играет мой шурин.
По сути, приведенный ниже код имитирует взаимодействия с монстрами в игре и как часто он может ожидать, что они потеряют предметы после их поражения. Проблема, с которой я сталкиваюсь, заключается в том, что я ожидал бы, что часть кода, подобная этому, идеально подходит для распараллеливания, но когда я добавляю в concurrency время, необходимое для выполнения всех симуляций, как правило, замедляется в 4-6 раз оригинал без concurrency.
Чтобы лучше понять, как работает код, у меня есть три основные функции: функция взаимодействия, которая представляет собой простое взаимодействие между игроком и монстром. Он возвращает 1, если монстр бросает элемент, а 0 - в противном случае. Функция имитации выполняет несколько взаимодействий и возвращает фрагмент результатов взаимодействия (т.е. 1 и 0 представляет успешные/неудачные взаимодействия). Наконец, есть тестовая функция, которая запускает набор симуляций и возвращает срез результатов моделирования, которые представляют собой общее количество взаимодействий, в результате которых выпал элемент. Это последняя функция, которую я пытаюсь запустить параллельно.
Теперь я понял, почему код замедляется, если я создаю goroutine для каждого теста, который я хочу запустить. Предполагая, что я запускаю 100 тестов, переключение контекста между каждым из goroutines по четырем процессорам, которые у меня MacBook Air убьет производительность, но я создаю столько же goroutines, сколько у меня есть процессоры, и деля количество тестов между goroutines. Я ожидал бы, что это фактически ускорит производительность кода, так как я выполняю каждый из своих тестов параллельно, но, конечно, я становлюсь основным замедлителем.
Мне бы хотелось выяснить, почему это происходит, поэтому нам будет очень благодарна любая помощь.
Ниже приведен обычный код без подпрограмм 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))
}
А вот вот параллельный код с goroutines:
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 для каждого goroutine в соответствии с предложением "система" ниже. Сейчас я вижу очень небольшую скорость по сравнению с серийной версией кода (около 15-20% сокращения общего времени). Мне бы очень хотелось узнать, почему я не вижу что-то ближе к 75-процентному сокращению времени, так как я распространяю рабочую нагрузку на свои ядра MBA 4. Есть ли у кого-нибудь дополнительные предложения, которые могли бы помочь?
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)
Спасибо всем за помощь в выяснении моей проблемы. Я, наконец, получил ответ, который искал, и поэтому я подумал, что просто подведу итог для всех, у кого есть такая же проблема.
По сути, у меня были две основные проблемы: во-первых, хотя мой код был неловко параллельным, он работал медленнее, когда я разбил его среди доступных процессоров, а второй, решение открыло еще одну проблему, и мой серийный код работал в два раза медленнее, чем параллельный код, работающий на одном процессоре, который, как вы ожидали, будет примерно одинаковым. В обоих случаях проблемой была функция генератора случайных чисел rand.Float64
. В принципе, это функция удобства, предоставляемая пакетом rand
. В этом пакете глобальный экземпляр структуры rand
создается и используется каждой из функций удобства. Этот глобальный экземпляр rand
имеет связанную с ним блокировку мьютекса. Поскольку я использовал эту функцию удобства, я действительно не мог распараллелить свой код, так как каждый из goroutines должен был выстраиваться в очередь для доступа к глобальному экземпляру rand
. Решение (как предлагает ниже система) заключается в создании отдельного экземпляра структуры rand
для каждого горутина. Это разрешило первую проблему, но создало вторую.
Вторая проблема заключалась в том, что мой непараллельный параллельный код (т.е. мой параллельный код, работающий только с одним процессором) работал в два раза быстрее, чем последовательный код. Причина этого заключалась в том, что, хотя я работал только с одним процессором и одним goroutine, у этого goroutine был свой собственный экземпляр структуры rand
, которую я создал, и я создал его без блокировки мьютекса. Последовательный код по-прежнему использовал функцию удобства rand.Float64
, которая использовала глобальный экземпляр rand
, защищенный мьютексом. Стоимость приобретения этого замка заставляла последовательный код работать в два раза медленнее.
Итак, мораль истории - всякий раз, когда важна производительность, убедитесь, что вы создаете экземпляр структуры rand
и вызываете нужную функцию, а не используете удобные функции, предоставляемые пакетом.