This version runs around 0.10
@Grapes(
@Grab(group='org.apache.commons', module='commons-math3', version='3.6.1')
)
import org.apache.commons.math3.random.MersenneTwister
@groovy.transform.CompileStatic
class Benchmark {
int benchmark(Closure closure) {
def start = System.currentTimeMillis()
closure.call()
System.currentTimeMillis() - start
}
def run() {
int N = 1000000
Map<Integer,Integer> results
int time = benchmark {
results = Twister.rolls(N)
}
println "Took ${time/1000} sec"
for (e in results.sort()) {
println "$e.key: $e.value"
}
}
}
@groovy.transform.CompileStatic
class Twister {
static MersenneTwister rng = new MersenneTwister()
static int roll() {
rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6) + 3
}
static Map<Integer,Integer> rolls(int num) {
Map<Integer,Integer> results = [:]
num.times {
int n = roll()
results[n] = results.containsKey(n) ? results[n] + 1 : 1
}
return results
}
}
new Benchmark().run()
> On Mar 28, 2017, at 5:41 PM, John Wagenleitner <[email protected]>
> wrote:
>
> Hi Paul,
>
> The milliseconds to seconds conversion was off, so that puts the real time at
> ~0.5 seconds.
>
> println "Took ${time/100} sec"
>
> Using the following I get somewhere close to 0.15. Using an int array may be
> worth it for higher values of N to avoid the boxing/unboxing of the ints.
>
> @Grapes(
> @Grab(group='org.apache.commons', module='commons-math3', version='3.6.1')
> )
> import org.apache.commons.math3.random.MersenneTwister
>
> def benchmark = { closure ->
> start = System.currentTimeMillis()
> closure.call()
> now = System.currentTimeMillis()
> now - start
> }
>
> @groovy.transform.CompileStatic
> class Twister {
> static MersenneTwister rng = new MersenneTwister()
>
> static int roll() {
> rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6) + 3
> }
>
> static Map<Integer,Integer> rolls(int num) {
> Map<Integer,Integer> results = [:]
> num.times {
> int n = roll()
> results[n] = results.containsKey(n) ? results[n] + 1 : 1
> }
> return results
> }
> }
>
> int N = 1000000
> def results
>
> def time = benchmark {
> results = Twister.rolls(N)
> }
>
> println "Took ${time/1000} sec"
> for (e in results.sort()) {
> println "$e.key: $e.value"
> }
>
>
>
> On Tue, Mar 28, 2017 at 1:25 PM, Paul Moore <[email protected]
> <mailto:[email protected]>> wrote:
> I'm very much a newbie with Groovy, so I apologise in advance if this
> is not the right place for questions like this. I couldn't find
> anywhere else that looked like a better option - if there is somewhere
> I should have asked, feel free to redirect me.
>
> I want to write a simulation script using Groovy - this is something
> of a hobby challenge for me, I have a friend who has done a similar
> task in C++, and I'm looking for a more user-friendly language to
> write the code, while not losing too much performance over the C
> version.
>
> The code is basically to simulate "a game". The particular game is
> defined by the user, as a function that generates scores. The program
> runs the game many times, and summarises the distribution of the
> results. Basically, a Monte Carlo simulation. My current code in
> Groovy for this, using "roll 3 dice and add up the results" as the
> target game, looks as follows:
>
> @Grapes(
> @Grab(group='org.apache.commons', module='commons-math3', version='3.6.1')
> )
> import org.apache.commons.math3.random.MersenneTwister
>
> def benchmark = { closure ->
> start = System.currentTimeMillis()
> closure.call()
> now = System.currentTimeMillis()
> now - start
> }
>
> rng = new MersenneTwister()
>
> int roll() {
> rng.nextInt(6) + rng.nextInt(6) + rng.nextInt(6) + 3
> }
>
> int N = 1000000
> def results = [:]
>
> def time = benchmark {
> N.times {
> int n = roll()
> results[n] = results.containsKey(n) ? results[n] + 1 : 1
> }
> }
>
> println "Took ${time/100} sec"
> for (e in results.sort()) {
> println "$e.key: $e.value"
> }
>
> This does exactly what I want, but takes about 5 seconds to run the
> simulation on my PC. My friend's C++ code runs a similar simulation in
> about 0.1 second. That's a massive penalty for Groovy, and likely
> means that for more realistic simulations (which would be a lot more
> complex than 3d6!) I wouldn't even be close to competitive.
>
> The code above is completely unoptimised. I know that Groovy's dynamic
> programming features can introduce some overhead, but I also get the
> impression from the documentation that by careful use of exact types,
> and similar techniques, this can be speeded up a lot (the docs claim
> potentially better than C performance in some cases).
>
> What should I be looking at to optimise the above code? The areas I
> can think of are:
>
> 1. The RNG. I assume that the apache commons code is pretty efficient,
> though. I do want a reasonably decent RNG, and I'd heard that the JVM
> RNG is not sufficient for simulation. For now I'm assuming that this
> is sufficiently good.
> 2. The roll() function. This is the core of the inner loop, and likely
> the big bottleneck. I've declared the type, which I guess is the first
> step, but I don't know what else I can do here. I tried a
> CompileStatic annotation, but that gave me errors about referencing
> the rng variable. I'm not sure what that implies - is my code doing
> something wrong in how it references the global rng variable?
> 3. Collecting the results in a map is likely not ideal. Is there a
> better data structure I should be using? I basically want to be able
> to count how many times each result appears - results will be integers
> (in certain cases I might want non-integers but I can handle them
> exceptionally) but I don't necessarily know the range in advance (so
> I'd avoid a static array unless it gives significant performance
> benefits - when I did a quick test, I got about a second faster
> runtime, noticeable, but not enough to get me anywhere near my sub-1
> second target)
>
> I tried using the GProf profiler to see if that shed any light on what
> I should do. When I ran with a realistic number of iterations it took
> forever and then failed with an out of memory error. Dropping it to
> 10000 iterations, I got
>
> % cumulative self self total self total
> self total
> time seconds seconds calls ms/call ms/call min ms min ms
> max ms max ms name
> 34.5 0.04 0.04 10000 0.00 0.00 0.00 0.00
> 0.91 1.13 blackpool$_run_closure2$_closure3.roll
> 18.5 0.06 0.02 39984 0.00 0.00 0.00 0.00
> 0.53 0.53 java.lang.Integer.plus
> 15.9 0.08 0.02 10000 0.00 0.01 0.00 0.00
> 0.28 1.26 blackpool$_run_closure2$_closure3.doCall
> 11.0 0.10 0.01 30000 0.00 0.00 0.00 0.00
> 0.36 0.36 org.apache.commons.math3.random.MersenneTwister.nextInt
> 5.1 0.10 0.00 10000 0.00 0.00 0.00 0.00
> 0.19 0.19 java.util.LinkedHashMap.containsKey
> 4.4 0.11 0.00 10000 0.00 0.00 0.00 0.00
> 0.02 0.02 java.util.LinkedHashMap.putAt
> 4.0 0.12 0.00 1 5.25 125.62 5.25 125.62
> 5.25 125.62 java.lang.Integer.times
> 3.9 0.12 0.00 9984 0.00 0.00 0.00 0.00
> 0.02 0.02 java.util.LinkedHashMap.getAt
> 2.2 0.12 0.00 1 2.85 128.48 2.85 128.48
> 2.85 128.48 blackpool$_run_closure2.doCall
>
> But I don't really know how to interpret that. (Also, am I somehow
> using GProf wrongly? It seems like it shouldn't run out of memory
> profiling a 5-second program run...)
>
> Can anyone offer any advice on what I should be looking at here?
>
> Thanks,
> Paul
>