fschuetz04 / simgo

Discrete-event simulation in Go using goroutines
https://pkg.go.dev/github.com/fschuetz04/simgo
MIT License
26 stars 4 forks source link

Simulation speed issue #2

Open JO-WTF opened 5 months ago

JO-WTF commented 5 months ago

I created the bank renege example model respectively with simpy and simgo.

What suprised me is that the run time of both models are quite similar, Python even faster for 10%.

I never used Go before, so wondering what the cause may be. Do you have any idea?

simpy code:

import time
import random

import simpy

RANDOM_SEED = 42
N_COUNTERS = 50
N_CUSTOMERS = 1000000  # Total number of customers
MEAN_ARRIVAL_INTERVAL = 10.0  # Generate new customers roughly every x seconds
PATIENCE = 16
MEAN_TIME_IN_BANK = 12.0

def source(env, number, interval, counter):
    """Source generates customers randomly"""
    for i in range(number):
        c = customer(env, f"Customer{i:02d}", counter, time_in_bank=MEAN_TIME_IN_BANK)
        env.process(c)
        t = random.expovariate(1.0 / interval)
        yield env.timeout(t)

def customer(env, name, counter, time_in_bank):
    """Customer arrives, is served and leaves."""

    with counter.request() as req:
        # Wait for the counter or abort at the end of our tether
        results = yield req | env.timeout(PATIENCE)

        if req in results:
            # We got to the counter
            tib = random.expovariate(1.0 / time_in_bank)
            yield env.timeout(tib)
        # else:
        # We reneged

# Setup and start the simulation
# print("Bank renege")
random.seed(RANDOM_SEED)
env = simpy.Environment()

# Start processes and run
counter = simpy.Resource(env, capacity=N_COUNTERS)
env.process(source(env, N_CUSTOMERS, MEAN_ARRIVAL_INTERVAL, counter))

start_time = time.time()
env.run()
end_time = time.time()
print("Elapsed time:", end_time - start_time)
print(env.now)

Python 3.12.1 output:

Elapsed time: 16.356430292129517

simgo code:

package main

import (
    "fmt"
    "math/rand"
    "time"

    "github.com/fschuetz04/simgo"
)

const (
    RandomSeed          = 42
    NCounters           = 50
    NCustomers          = 1000000
    MeanArrivalInterval = 10
    MaxWaitTime         = 16
    MeanTimeInBank      = 12
)

func customerSource(proc simgo.Process) {
    counters := NewResource(proc, NCounters)

    for id := 1; id <= NCustomers; id++ {
        proc.ProcessReflect(customer, id, counters)
        delay := rand.ExpFloat64() * MeanArrivalInterval
        proc.Wait(proc.Timeout(delay))
    }
}

func customer(proc simgo.Process, id int, counters *Resource) {

    request := counters.Request()
    timeout := proc.Timeout(MaxWaitTime)
    proc.Wait(proc.AnyOf(request, timeout))

    if !request.Triggered() {
        request.Abort()
        return
    }

    delay := rand.ExpFloat64() * MeanTimeInBank
    proc.Wait(proc.Timeout(delay))

    counters.Release()
}

func timeCost() func() {
    start := time.Now()
    return func() {
        tc := time.Since(start)
        fmt.Printf("time cost = %v\n", tc)
    }
}

func runSim(sim *simgo.Simulation) {
    defer timeCost()()
    sim.Run()
    fmt.Printf("%.f\n", sim.Now())
}

func main() {
    rand.Seed(RandomSeed)

    sim := simgo.Simulation{}

    sim.Process(customerSource)

    runSim(&sim)
}

Golang go version go1.22.2 linux/amd64 output:

time cost = 18.406199488s
fschuetz04 commented 4 months ago

On my machine, both versions take around 6.5 seconds, but the Python version is indeed a bit faster on average.

Unfortunately, Go doesn't offer Coroutines, so instead I use one Goroutine per process. The Goroutines are synchronized via channels, so effectively only one runs at any given time. You might find https://research.swtch.com/coro interesting. In the case of the bank example, one process per customer is created, so the Go version needs 1 million Goroutines. Creating them and constantly switching between them of course impacts performance quite a bit.

Compared to Go, Python offers Generators, which are used to make SimPy work without creating a thread or something similar per process. Even if Python is much slower than Go in principle, this leads to the similar performance for this example.

If you want better performance right now, you might want to try the C++ version of this library: https://github.com/fschuetz04/simcpp20. On my machine, the same bank example with 1 million customers takes about 0.5 seconds :)

JO-WTF commented 3 months ago

Thanks for your reply.

I tried simcpp20 actually. However, not every simpy functionality is implemented in simcpp20 yet (store, container for example), so it seems there are a lot work to do before it can be applied to a project to replace simpy.