Skip to content

Commit 599c791

Browse files
committed
Lab5 expanded ecosystem profiling.
1 parent c74068e commit 599c791

File tree

2 files changed

+204
-78
lines changed

2 files changed

+204
-78
lines changed

docs/src/lecture_05/lab.md

Lines changed: 180 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -302,40 +302,46 @@ ProfileSVG.save("./scalar_prof_horner.svg") #hide
302302
```
303303

304304
## Ecosystem debugging
305-
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.
306306

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
308312
using Profile, ProfileSVG
309-
using EcosystemCore
310313
311-
n_grass = 500
312-
regrowth_time = 17.0
314+
function create_world()
315+
n_grass = 500
316+
regrowth_time = 17.0
313317
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
318322
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
323327
324-
gs = [Grass(id, regrowth_time) for id in 1: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+
```
328336

329-
# precompile everything
330-
simulate!(w, 1, [w -> @info agent_count(w)])
337+
Precompile everything by running one step of our simulation and run the profiler.
331338

332-
@profview simulate!(w, 100, [w -> @info agent_count(w)])
333-
ProfileSVG.save("./ecosystem.svg")
339+
```@example lab05_ecosystem
340+
simulate!(world, 1)
341+
@profview simulate!(world, 100)
334342
```
335-
![ecosystem_unstable](./ecosystem.svg)
336-
337343

338-
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.
339345

340346
```julia
341347
# original
@@ -345,142 +351,238 @@ function EcosystemCore.find_rand(f, w::World)
345351
end
346352
```
347353

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.
349355

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
352360
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
354362
@code_warntype EcosystemCore.find_rand(f, world) # check type stability
355363
```
356364

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)
361369
```
362370

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.
372+
363373
```@raw html
364374
<div class="admonition is-category-exercise">
365375
<header class="admonition-header">Exercise</header>
366376
<div class="admonition-body">
367377
```
378+
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.
368379

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
370381
```julia
371382
function EcosystemCore.find_rand(f, w::World)
372383
...
373384
end
374385
```
375-
that has the same functionality, while not having return type of `Any`.
376386

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].
388+
389+
[^2]: Reservoir sampling [https://en.wikipedia.org/wiki/Reservoir\_sampling](https://en.wikipedia.org/wiki/Reservoir_sampling)
390+
[^3]: [https://stackoverflow.com/q/9690009](https://stackoverflow.com/q/9690009)
378391

379392
```@raw html
380393
</div></div>
381394
<details class = "solution-body">
382395
<summary class = "solution-header">Solution:</summary><p>
383396
```
397+
There are a few alterations, which we can try.
384398

385-
```julia
386-
# Vasek
399+
```@example lab05_ecosystem
400+
using StatsBase: shuffle!
387401
function EcosystemCore.find_rand(f, w::World)
388-
ks = collect(keys(w.agents))
389-
for i in randperm(length(w.agents))
390-
a = w.agents[ks[i]]
402+
for i in shuffle!(collect(keys(w.agents)))
403+
a = w.agents[i]
391404
f(a) && return a
392405
end
393406
return nothing
394407
end
408+
```
395409

396-
# mine
410+
```@example lab05_ecosystem
397411
function EcosystemCore.find_rand(f, w::World)
398-
for i in shuffle!(collect(keys(w.agents)))
399-
a = w.agents[i]
400-
f(a) && return a
412+
count = 1
413+
selected = nothing
414+
for a in values(w.agents)
415+
if f(a)
416+
if rand() * count < 1
417+
selected = a
418+
end
419+
count += 1
420+
end
401421
end
402-
return nothing
422+
selected
403423
end
404424
```
405-
![ecosystem_stablish](./ecosystem_1.svg)
425+
426+
Let's profile the simulation again
427+
```@example lab05_ecosystem
428+
world = create_world();
429+
simulate!(world, 1)
430+
@profview simulate!(world, 100)
431+
ProfileSVG.save("./ecosystem_nofilter.svg")
432+
```
433+
![profile_ecosystem_nofilter](./ecosystem_nofilter.svg)
406434

407435
```@raw html
408436
</p></details>
409437
```
410438

439+
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.
411440

412441
```@raw html
413442
<div class="admonition is-category-exercise">
414443
<header class="admonition-header">Exercise</header>
415444
<div class="admonition-body">
416445
```
446+
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.
417447

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
419451

420452
```@raw html
421453
</div></div>
422454
<details class = "solution-body">
423455
<summary class = "solution-header">Solution:</summary><p>
424456
```
425457

426-
```julia
427-
using BenchmarkTools
428-
429-
gs = [Grass(id, regrowth_time) for id in 1: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)
435-
# https://github.com/JuliaCI/BenchmarkTools.jl/issues/44
436-
@benchmark begin
437-
Random.seed!(7);
438-
simulate!(World(vcat(g, s, w)), 10)
439-
end setup=(g = copy(gs), s = copy(ss), w = copy(ws))
440-
441-
442-
# begin and end has to be used
443-
# also deepcopy is needed
458+
Run the following code for each version to find some differences.
459+
```@example lab05_ecosystem
460+
using BenchmarkTools #hide
461+
using Random
462+
world = create_world();
444463
@benchmark begin
445464
Random.seed!(7);
446-
simulate!(World(vcat(g, s, w)), 10)
447-
end setup=begin g, s, w = deepcopy(gs), deepcopy(ss), deepcopy(ws) end
448-
449-
450-
@benchmark simulate!(World(vcat(g, s, w)), 10) setup=(g=copy(gs))
465+
simulate!(w, 10)
466+
end setup=(w=deepcopy($world)) evals=1 samples=20 seconds=30
451467
```
468+
Recall that when using `setup`, we have to limit number of evaluations to `evals=1` in order not to mutate the `world` struct.
452469

453470
```@raw html
454471
</p></details>
455472
```
456473

457474
### 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}`
476+
- `user` - measure memory allocation everywhere except Julia's core code
477+
- `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+
```
461487

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.
463489

464490
```@raw html
465491
<div class="admonition is-category-exercise">
466492
<header class="admonition-header">Exercise</header>
467493
<div class="admonition-body">
468494
```
469495

470-
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
471497
```bash
472-
julia -L ./your_script.jl --track-allocation={all|user}
498+
julia --track-allocation=user
473499
```
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?
474501

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`.
476504
```@raw html
477505
</div></div>
478506
<details class = "solution-body">
479507
<summary class = "solution-header">Solution:</summary><p>
480508
```
481509

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+
function create_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 in 1: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+
```
483536

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.
546+
```julia
547+
julia> using Coverage
548+
julia> analyze_malloc(expanduser("~/.julia/packages/EcosystemCore/8dzJF/src"))
549+
35-element Vector{CoverageTools.MallocInfo}:
550+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 21)
551+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 22)
552+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 24)
553+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 26)
554+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 27)
555+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 28)
556+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 30)
557+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 31)
558+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 33)
559+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 38)
560+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 41)
561+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 48)
562+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 49)
563+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 59)
564+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 60)
565+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 62)
566+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 64)
567+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 65)
568+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/plant.jl.498486.mem", 16)
569+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/plant.jl.498486.mem", 17)
570+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 14)
571+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 15)
572+
CoverageTools.MallocInfo(0, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 16)
573+
CoverageTools.MallocInfo(16, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 9)
574+
CoverageTools.MallocInfo(32, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 2)
575+
CoverageTools.MallocInfo(32, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 8)
576+
CoverageTools.MallocInfo(288, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 7)
577+
CoverageTools.MallocInfo(3840, "~/.julia/packages/EcosystemCore/8dzJF/src/world.jl.498486.mem", 13)
578+
CoverageTools.MallocInfo(32000, "~/.julia/packages/EcosystemCore/8dzJF/src/plant.jl.498486.mem", 13)
579+
CoverageTools.MallocInfo(69104, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 23)
580+
CoverageTools.MallocInfo(81408, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 58)
581+
CoverageTools.MallocInfo(244224, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 2)
582+
CoverageTools.MallocInfo(488448, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 63)
583+
CoverageTools.MallocInfo(895488, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 61)
584+
CoverageTools.MallocInfo(229589792, "~/.julia/packages/EcosystemCore/8dzJF/src/animal.jl.498486.mem", 37)
585+
```
484586

485587
```@raw html
486588
</p></details>

docs/src/lecture_05/sim.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# using Ecosystem
2+
using Scientific_Programming_in_Julia.Ecosystem
3+
4+
function create_world()
5+
n_grass = 500
6+
regrowth_time = 17.0
7+
8+
n_sheep = 100
9+
Δenergy_sheep = 5.0
10+
sheep_reproduce = 0.5
11+
sheep_foodprob = 0.4
12+
13+
n_wolves = 8
14+
Δenergy_wolf = 17.0
15+
wolf_reproduce = 0.03
16+
wolf_foodprob = 0.02
17+
18+
gs = [Grass(id, regrowth_time) for id in 1:n_grass];
19+
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
20+
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];
21+
World(vcat(gs, ss, ws))
22+
end
23+
world = create_world();
24+
simulate!(world, 10)

0 commit comments

Comments
 (0)