-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from JuliaComputing/fb/tutorials
Add tutorials
- Loading branch information
Showing
9 changed files
with
343 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,8 @@ | |
|
||
```@docs | ||
AllocCheck.check_allocs | ||
``` | ||
``` | ||
|
||
```@docs | ||
AllocCheck.@check_allocs | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Guaranteed Error Recovery | ||
|
||
Safety-critical real-time systems are often required to have performance critical error-recovery logic. While errors are not supposed to occur, they sometimes do anyways 😦, and when they do, we may want to make sure that the recovery logic runs with minimum latency. | ||
|
||
In the following example, we are executing a loop that may throw an error. By default [`check_allocs`](@ref) allows allocations on the error path, i.e., allocations that occur as a consequence of an exception being thrown. This can cause the garbage collector to be invoked by the allocation, and introduce an unbounded latency before we execute the error recovery logic. | ||
|
||
To guard ourselves against this, we may follow these steps | ||
1. Prove that the function does not allocate memory except for on exception paths. | ||
2. Since we have proved that we are not allocating memory, we may disable the garbage collector. This prevents it from running before the error recovery logic. | ||
3. To make sure that the garbage collector is re-enabled after an error has been recovered from, we re-enable it in a `finally` block. | ||
|
||
|
||
|
||
```@example ERROR | ||
function treading_lightly() | ||
a = 0.0 | ||
GC.enable(false) # Turn off the GC before entering the loop | ||
try | ||
for i = 10:-1:-1 | ||
a += sqrt(i) # This throws an error for negative values of i | ||
end | ||
catch | ||
exit_gracefully() # This function is supposed to run with minimum latency | ||
finally | ||
GC.enable(true) # Always turn the GC back on before exiting the function | ||
end | ||
a | ||
end | ||
exit_gracefully() = println("Calling mother") | ||
using AllocCheck, Test | ||
allocs = check_allocs(treading_lightly, ()) # Check that it's safe to proceed | ||
``` | ||
```@example ERROR | ||
@test isempty(allocs) | ||
``` | ||
|
||
[`check_allocs`](@ref) returned zero allocations. If we invoke [`check_allocs`](@ref) with the flag `ignore_throw = false`, we will see that the function may allocate memory on the error path: | ||
|
||
```@example ERROR | ||
allocs = check_allocs(treading_lightly, (); ignore_throw = false) | ||
length(allocs) | ||
``` | ||
|
||
Finally, we test that the function is producing the expected result: | ||
|
||
```@example ERROR | ||
val = treading_lightly() | ||
@test val ≈ 22.468278186204103 # hide | ||
``` | ||
|
||
In this example, we accepted an allocation on the exception path with the motivation that it occurred once only, after which the program was terminated. Implicit in this approach is an assumption that the exception path does not allocate too much memory to execute the error recovery logic before the garbage collector is turned back on. We should thus convince ourselves that this assumption is valid, e.g., by means of testing: | ||
|
||
```@example ERROR | ||
treading_lightly() # Warm start | ||
allocated_memory = @allocated treading_lightly() # A call that triggers the exception path | ||
# @test allocated_memory < 1e4 | ||
``` | ||
|
||
The allocations sites reported with the flag `ignore_throw = false` may be used as a guide as to what to test. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# Allocations followed by a hot loop | ||
A common pattern in high-performance Julia code, as well as in real-time systems, is to initially allocate some working memory, followed by the execution of a performance sensitive _hot loop_ that should perform no allocations. In the example below, we show a function `run_almost_forever` that resembles the implementation of a simple control system. The function starts by allocating a large `logvector` in which some measurement data is to be saved, followed by the execution of a loop which should run with as predictable timing as possible, i.e., we do not want to perform any allocations or invoke the garbage collector while executing the loop. | ||
```@example HOT_LOOP | ||
function run_almost_forever() | ||
N = 100_000 # A large number | ||
logvector = zeros(N) # Allocate a large vector for storing results | ||
for i = 1:N # Run a hot loop that may not allocate | ||
y = sample_measurement() | ||
logvector[i] = y | ||
u = controller(y) | ||
apply_control(u) | ||
Libc.systemsleep(0.01) | ||
end | ||
end | ||
# Silly implementations of the functions used in the example | ||
sample_measurement() = 2.0 | ||
controller(y) = -2y | ||
apply_control(u) = nothing | ||
nothing # hide | ||
``` | ||
|
||
Here, the primary concern is the loop, while the preamble of the function should be allowed to allocate memory. The recommended strategy in this case is to refactor the function into a separate preamble and loop, like this | ||
```@example HOT_LOOP | ||
function run_almost_forever2() # The preamble that performs allocations | ||
N = 100_000 # A large number | ||
logvector = zeros(N) # Allocate a large vector for storing results | ||
run_almost_forever!(logvector) | ||
end | ||
function run_almost_forever!(logvector) # The hot loop that is allocation free | ||
for i = eachindex(logvector) # Run a hot loop that may not allocate | ||
y = sample_measurement() | ||
@inbounds logvector[i] = y | ||
u = controller(y) | ||
apply_control(u) | ||
Libc.systemsleep(0.01) | ||
end | ||
end | ||
nothing # hide | ||
``` | ||
|
||
We may now analyze the loop function `run_almost_forever!` to verify that it does not allocate memory: | ||
```@example HOT_LOOP | ||
using AllocCheck, Test | ||
allocs = check_allocs(run_almost_forever!, (Vector{Float64},)); | ||
@test isempty(allocs) | ||
``` | ||
|
||
|
||
## More complicated initialization | ||
In practice, a function may need to perform several distinct allocations upfront, including potentially allocating objects of potentially complicated types, like closures etc. In situations like this, the following pattern may be useful: | ||
```julia | ||
struct Workspace | ||
# All you need to run the hot loop, for example: | ||
cache1::Vector{Float64} | ||
cache2::Matrix{Float64} | ||
end | ||
|
||
function setup(max_iterations::Int = 100_000) | ||
# Allocate and initialize the workspace | ||
cache1 = zeros(max_iterations) | ||
cache2 = zeros(max_iterations, max_iterations) | ||
return Workspace(cache1, cache2) | ||
end | ||
|
||
function run!(workspace::Workspace) | ||
# The hot loop | ||
for i = eachindex(workspace.cache1) | ||
workspace.cache1[i] = my_important_calculation() # The allocated cache is modified in place | ||
... | ||
end | ||
end | ||
|
||
function run() | ||
workspace = setup() | ||
run!(workspace) | ||
end | ||
``` | ||
|
||
Here, `workspace` is a custom struct designed to serve as a workspace for the hot loop, but it could also be realized as a simple tuple of all the allocated objects required for the computations. Note, the struct `Workspace` in this example was not marked as mutable. However, its contents, the two cache arrays, are. This means that the `run!` function may modify the contents of the cache arrays. | ||
|
||
The benefit of breaking the function up into two parts which are called from a third, is that we may now create the workspace object individually, and use it to compute the type of the arguments to the `run!` function that we are interested in analyzing: | ||
```julia | ||
workspace = setup() | ||
allocs = check_allocs(run!, (typeof(workspace),)) | ||
``` |
Oops, something went wrong.