Skip to content

Commit 907d626

Browse files
committed
working on lecture 5
1 parent be5cce0 commit 907d626

File tree

2 files changed

+567
-49
lines changed

2 files changed

+567
-49
lines changed

docs/src/lecture_05/lecture.md

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -857,73 +857,70 @@ So when you use closures, you should be careful of the accidental boxing, since
857857
It happens a lot in scientific code, that some experiments has many parameters. It is therefore very convenient to store them in `Dict`, such that when adding a new parameter, we do not have to go over all defined functions and redefine them.
858858
859859
Imagine that we have a (nonsensical) simulation like
860-
```
861-
settings = Dict(:stepsize => 0.01, :h => 0.001, :iters => 500)
860+
```julia
861+
settings = Dict(:stepsize => 0.01, :h => 0.001, :iters => 500, :info => "info")
862862
function find_min!(f, x, p)
863863
for i in 1:p[:iters]
864864
= x + p[:h]
865-
fx = f(x)
866-
x -= p[:stepsize] * (f(x̃) - fx)/p[:h]
865+
fx = f(x) # line 4
866+
x -= p[:stepsize] * (f(x̃) - fx)/p[:h] # line 5
867867
end
868868
x
869869
end
870870
```
871-
872-
873-
871+
Notice the parameter `p` is a `Dict` and notice that it can contain arbitrary parameters, which is useful. Hence, Dict is cool for passing parameters.
872+
Let's now run the function through the profiler
873+
```julia
874874
x₀ = rand()
875-
f = x -> x^2
876-
find_min!(f, x₀, params_tuple)
877-
878-
params_tuple = (;stepsize = 0.01, h=0.001, iters=500)
879-
@btime find_min!($f, $x₀, $params_dict)
880-
@btime find_min!($f, $x₀, $params_tuple)
875+
f(x) = x^2
876+
Profile.clear()
877+
@profile find_min!(f, x₀, settings)
878+
ProfileSVG.save("/tmp/profile6.svg")
881879
```
882-
- performance tweaks
883-
+ differnce between named tuple and dict
884-
+ IO
880+
from the profiler's output [here](profile6.svg) we can see some type instabilities. Where they come from?
881+
The compiler does not have any infomation about types stored in `settings`, as the type of stored values are `Any` (caused by storing `String` and `Int`).
882+
```julia
883+
julia> typeof(settings)
884+
Dict{Symbol, Any}
885885
```
886-
887-
- create a list of examples for the lecture
888-
889-
## Using named tuple instead of dict
886+
The second problem is `get` operation on dictionaries is very time consuming operation (although technically it is O(1)), because it has to search the key in the list. Dicts are designed as a mutable container, which is not needed in our use-case, as the settings are static. For similar use-cases, Julia offers `NamedTuple`, with which we can construct settings as
890887
```julia
891-
892-
## Performance of captured variable
893-
- inspired by https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-captured
894-
- an example of closure seen in previous lectures, reference `x` has to be included
888+
nt_settings = (;stepsize = 0.01, h=0.001, iters=500, :info => "info")
889+
```
890+
The `NamedTuple` is fully typed, but which we mean the names of fields are part of the type definition and fields are also part of type definition. You can think of it as a struct. Moreover, when accessing fields in `NamedTuple`, compiler knows precisely where they are located in the memory, which drastically reduces the access time.
891+
Let's see the effect in `BenchmarkTools`.
895892
```julia
896-
x = rand(1000)
893+
julia> @benchmark find_min!(x -> x^2, x₀, settings)
894+
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
895+
Range (min max): 86.350 μs 4.814 ms ┊ GC (min max): 0.00% 97.61%
896+
Time (median): 90.747 μs ┊ GC (median): 0.00%
897+
Time (mean ± σ): 102.405 μs ± 127.653 μs ┊ GC (mean ± σ): 4.69% ± 3.75%
897898

898-
function adder(shift)
899-
return y -> shift + y
900-
end
899+
▅██▆▂ ▁▁ ▁ ▂
900+
███████▇▇████▇███▇█▇████▇▇▆▆▇▆▇▇▇▆▆▆▆▇▆▇▇▅▇▆▆▆▆▄▅▅▄▅▆▆▅▄▅▃▅▃▅ █
901+
86.4 μs Histogram: log(frequency) by time 209 μs <
901902

902-
function adder_typed(shift::Float64)
903-
return y -> shift + y
904-
end
903+
Memory estimate: 70.36 KiB, allocs estimate: 4002.
905904

906-
function adder_let(shift::Float64)
907-
f = let shift=shift
908-
y -> shift + y
909-
end
910-
return f
911-
end
905+
julia> @benchmark find_min!(x -> x^2, x₀, nt_settings)
906+
BenchmarkTools.Trial: 10000 samples with 7 evaluations.
907+
Range (min max): 4.179 μs 21.306 μs ┊ GC (min max): 0.00% 0.00%
908+
Time (median): 4.188 μs ┊ GC (median): 0.00%
909+
Time (mean ± σ): 4.493 μs ± 1.135 μs ┊ GC (mean ± σ): 0.00% ± 0.00%
912910

913-
f = adder(3.0)
914-
ft = adder_typed(3.0)
915-
fl = adder_let(3.0)
916-
917-
@btime f.($x);
918-
@btime ft.($x);
919-
@btime fl.($x);
920-
@btime $x .+ 3.0;
911+
█▃▁ ▁ ▁ ▁ ▁
912+
████▇████▄██▄█▃██▄▄▇▇▇▇▅▆▆▅▄▄▅▄▅▅▅▄▁▅▄▁▄▄▆▆▇▄▅▆▄▄▃▄▆▅▆▁▄▄▄ █
913+
4.18 μs Histogram: log(frequency) by time 10.8 μs <
921914

915+
Memory estimate: 16 bytes, allocs estimate: 1.
922916
```
923-
- cannot get the same performance as native call, might be affected by broadcasting (?)
924-
- `fl` should attain the same performance as native call
925-
926917
918+
Checking the output with JET, there is no type instability anymore
919+
```julia
920+
@report_opt find_min!(f, x₀, nt_settings)
921+
No errors !
922+
```
923+
<!--
927924
## Don't use IO unless you have to
928925
- debug printing in performance critical code should be kept to minimum or using in memory/file based logger in stdlib `Logging.jl`
929926
```julia
@@ -952,4 +949,4 @@ function find_min!(f, x, p; verbose=true)
952949
x
953950
end
954951
@btime find_min!($f, $x₀, $params_tuple; verbose=true)
955-
```
952+
``` -->

0 commit comments

Comments
 (0)