You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Let's now apply what we have learned so far on the much bigger codebase of our `Ecosystem` and `EcosystemCore` packages.
305
+
Let's now apply what we have learned so far on the much bigger codebase of our `Ecosystem` and `EcosystemCore` packages.
306
306
307
-
```julia
307
+
!!! note "Installation of Ecosystem pkg"
308
+
If you do not have Ecosystem readily available you can get it from our [repository](https://github.com/JuliaTeachingCTU/Scientific-Programming-in-Julia/blob/master/src/Ecosystem.jl).
309
+
310
+
```@example lab05_ecosystem
311
+
using Scientific_Programming_in_Julia.Ecosystem #hide
308
312
using Profile, ProfileSVG
309
-
using EcosystemCore
310
313
311
-
n_grass =500
312
-
regrowth_time =17.0
314
+
function create_world()
315
+
n_grass = 500
316
+
regrowth_time = 17.0
313
317
314
-
n_sheep =100
315
-
Δenergy_sheep =5.0
316
-
sheep_reproduce =0.5
317
-
sheep_foodprob =0.4
318
+
n_sheep = 100
319
+
Δenergy_sheep = 5.0
320
+
sheep_reproduce = 0.5
321
+
sheep_foodprob = 0.4
318
322
319
-
n_wolves =8
320
-
Δenergy_wolf =17.0
321
-
wolf_reproduce =0.03
322
-
wolf_foodprob =0.02
323
+
n_wolves = 8
324
+
Δenergy_wolf = 17.0
325
+
wolf_reproduce = 0.03
326
+
wolf_foodprob = 0.02
323
327
324
-
gs = [Grass(id, regrowth_time) for id in1:n_grass];
325
-
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
326
-
ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves];
327
-
world =World(vcat(gs, ss, ws));
328
+
gs = [Grass(id, regrowth_time) for id in 1:n_grass];
329
+
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
330
+
ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves];
331
+
World(vcat(gs, ss, ws))
332
+
end
333
+
world = create_world();
334
+
nothing #hide
335
+
```
328
336
329
-
# precompile everything
330
-
simulate!(w, 1, [w ->@infoagent_count(w)])
337
+
Precompile everything by running one step of our simulation and run the profiler.
By investigating the top bars we see that most of the time is spend inside `EcosystemCore.find_rand`, either when called from `EcosystemCore.find_food` or `EcosystemCore.find_mate`.
344
+
Red bars indicate type instabilities however, unless the bars stacked on top of them are high, narrow and not filling the whole width, the problem should not be that serious. In our case the worst offender is the`filter` method inside `EcosystemCore.find_rand` function, either when called from `EcosystemCore.find_food` or `EcosystemCore.find_mate`. In both cases the bars on top of it are narrow and not the full with, meaning that not that much time has been really spend working, but instead inferring the types in the function itself during runtime.
339
345
340
346
```julia
341
347
# original
@@ -345,142 +351,238 @@ function EcosystemCore.find_rand(f, w::World)
345
351
end
346
352
```
347
353
348
-
Looking at the original code, we may not know exactly what is the problem, however the red color indicates that the code may be type unstable. Let's confirm the suspicion by evaluation.
354
+
Looking at the original [code](https://github.com/JuliaTeachingCTU/EcosystemCore.jl/blob/359f0b48314f9aa3d5d8fa0c85eebf376810aca6/src/animal.jl#L36-L39), we may not know exactly what is the problem, however the red color indicates that the code may be type unstable. Let's see if that is the case by evaluation the function with some isolated inputs.
349
355
350
-
```julia
351
-
w = ws[1] # get an instance of a wolf
356
+
```@example lab05_ecosystem
357
+
using InteractiveUtils #hide
358
+
using Scientific_Programming_in_Julia.Ecosystem.EcosystemCore #hide
359
+
w = Wolf(1, 20.0, 10.0, 0.9, 0.75) # get an instance of a wolf
352
360
f = x -> EcosystemCore.eats(w, x) # define the filter function used in the `find_rand`
353
-
EcosystemCore.find_rand(f, world)
361
+
EcosystemCore.find_rand(f, world) # check that it returns what we want
354
362
@code_warntype EcosystemCore.find_rand(f, world) # check type stability
355
363
```
356
364
357
-
Indeed we see that the function is type unstable. As a resulting in the other two functions to be type unstable
358
-
```julia
359
-
@code_warntype EcosystemCore.find_food(w, world)# unstable as expected
360
-
@code_warntype EcosystemCore.find_mate(w, world)# unstable as expected
365
+
Indeed we see that the return type is not inferred precisely but ends up being just the `Union{Nothing, Agent}`, however this is better than straight out `Any`, which is the union of all types and thus the compiler has to search much wider space. This uncertainty is propagated further resulting in the two parent functions to be inferred imperfectly.
366
+
```@repl lab05_ecosystem
367
+
@code_warntype EcosystemCore.find_food(w, world)
368
+
@code_warntype EcosystemCore.find_mate(w, world)
361
369
```
362
370
371
+
The underlying issue here is that we are enumerating over an array of type `Vector{Agent}`, where `Agent` is abstract, which does not allow Julia compiler to specialize the code for the loop body as it has to always check first the type of the item in the vector. This is even more pronounced in the `filter` function that filters the array by creating a copy of their elements, thus needing to know what the resulting array should look like.
Replace the `filter` function in `EcosystemCore.find_rand` with a different mechanism, which does not suffer from the same performance problems as viewed by the profiler. Use the simulation of 100 steps to see the difference.
368
379
369
-
Try to fix the type instability in `EcosystemCore.find_rand`by redefining it in the current session, i.e. write function
380
+
Use temporary patching by redefine the function in the current REPL session, i.e. write the function fully specified
370
381
```julia
371
382
function EcosystemCore.find_rand(f, w::World)
372
383
...
373
384
end
374
385
```
375
-
that has the same functionality, while not having return type of `Any`.
376
386
377
-
**HINT**: With the current design of the whole package we cannot really get anything better than `Union{Agent, Nothing}`
387
+
**BONUS**: Explore the algorithmic side of things by implementing a different sampling strategies [^2][^3].
We have tried few variants, however none of them really gets rid of the underlying problem. The solution unfortunately requires rewriting the World type, with a different container, that would store each species in a separate container such that the iteration never goes over an array of mixed types. Having said this we may still be interested in a solution that performs the best, given the current architecture.
Benchmark different versions of the `find_rand` function in a simulation 10 steps. In order for this comparison to be fair, we need to ensure that both the initial state of the `World` as well as all calls to the `Random` library stay the same.
417
447
418
-
How big of a performance penalty did we have to pay? Benchmark the simulation against the original version and the improved version.
448
+
**HINTS**:
449
+
- use `Random.seed!` to fix the global random number generator before each run of simulation
450
+
- use `setup` keyword and `deepcopy` to initiate the `world` variable to the same state in each evaluation
419
451
420
452
```@raw html
421
453
</div></div>
422
454
<details class = "solution-body">
423
455
<summary class = "solution-header">Solution:</summary><p>
424
456
```
425
457
426
-
```julia
427
-
using BenchmarkTools
428
-
429
-
gs = [Grass(id, regrowth_time) for id in1:n_grass];
430
-
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
431
-
ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves];
432
-
world =World(vcat(gs, ss, ws));
433
-
434
-
# does not work with the setup (works only with one setup variable)
end setup=(w=deepcopy($world)) evals=1 samples=20 seconds=30
451
467
```
468
+
Recall that when using `setup`, we have to limit number of evaluations to `evals=1` in order not to mutate the `world` struct.
452
469
453
470
```@raw html
454
471
</p></details>
455
472
```
456
473
457
474
### Tracking allocations
458
-
Memory allocation is oftentimes the most CPU heavy part of the computation, thus working with memory correctly, i.e. avoiding unnecessary allocation is key for a well performing code. In order to get a sense of how much memory is allocated at individual places of the your codebase, we can instruct Julia to keep track of the allocations with a command line option `--track-allocation={all|user}`*figure out what these options do*
459
-
- all
460
-
- user
475
+
Memory allocation is oftentimes the most CPU heavy part of the computation, thus working with memory correctly, i.e. avoiding unnecessary allocation is key for a well performing code. In order to get a sense of how much memory is allocated at individual places of the your codebase, we can instruct Julia to keep track of the allocations with a command line option `--track-allocation={all|user}`
-`all` - measure memory allocation at each line of Julia code
478
+
479
+
After exiting, Julia will create a copy of each source file, that has been touched during execution and assign to each line the number of allocations in bytes. In order to avoid including allocation from compilation the memory allocation statistics have to be cleared after first run by calling `Profile.clear_malloc_data()`, resulting in this kind of workflow
480
+
```julia
481
+
using Profile
482
+
run_code()
483
+
Profile.clear_malloc_data()
484
+
run_code()
485
+
# exit julia
486
+
```
461
487
462
-
After exiting, Julia will create a copy of each source file, that has been touched during execution and assign to each line the number of allocations in bytes.
488
+
`run_code` can be replaced by inclusion of a script file, which will be the annotated as well.
Transform the simulation code above into a script. Run this script with Julia with the `--track-allocation={all|user}` option, i.e.
496
+
Transform the simulation code above into a script and include it into a new Julia session
471
497
```bash
472
-
julia -L ./your_script.jl --track-allocation={all|user}
498
+
julia --track-allocation=user
473
499
```
500
+
Use the steps above to obtain a memory allocation map. Investigate the results of allocation tracking inside `EcosystemCore` source files. Where is the line with the most allocations?
474
501
475
-
Investigate the results of allocation tracking inside `EcosystemCore` source files. Where is the line with the most allocations?
502
+
**BONUS**
503
+
Use pkg `Coverage.jl` to process the resulting files from withing the `EcosystemCore`.
476
504
```@raw html
477
505
</div></div>
478
506
<details class = "solution-body">
479
507
<summary class = "solution-header">Solution:</summary><p>
480
508
```
481
509
482
-
I would expect that the same piece of code that has been type unstable also shows the allocations - the line inside `find_rand` that contains `filter, collect, keys, etc.`. *CHECK*
510
+
The [script](https://github.com/JuliaTeachingCTU/Scientific-Programming-in-Julia/blob/master/docs/src/lecture_05/sim.jl) called `sim.jl`
511
+
```julia
512
+
using Ecosystem
513
+
514
+
functioncreate_world()
515
+
n_grass =500
516
+
regrowth_time =17.0
517
+
518
+
n_sheep =100
519
+
Δenergy_sheep =5.0
520
+
sheep_reproduce =0.5
521
+
sheep_foodprob =0.4
522
+
523
+
n_wolves =8
524
+
Δenergy_wolf =17.0
525
+
wolf_reproduce =0.03
526
+
wolf_foodprob =0.02
527
+
528
+
gs = [Grass(id, regrowth_time) for id in1:n_grass];
529
+
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
530
+
ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves];
531
+
World(vcat(gs, ss, ws))
532
+
end
533
+
world =create_world();
534
+
simulate!(world, 10)
535
+
```
483
536
537
+
How to run.
538
+
```julia
539
+
using Profile
540
+
include("./sim.jl")
541
+
Profile.clear_malloc_data()
542
+
include("./sim.jl")
543
+
```
544
+
545
+
Pkg `Coverage.jl` can highlight where is the problem with allocations.
0 commit comments