Containers.jl
Documentation for Containers.jl.
Container creation
All you need to know to create a new mixed-type container using Containers.jl
is how to use the @container
macro. The user have the possibility to create both named and unnamed fields within the user-defined container. In the latter case, a default name will be assigned during the container creation.
The basic syntax to be used to define a container is:
@container "ContainerName" begin
"field-1" → T1,
"field-2" → T2(1.0),
T1(1.0),
T2
end
Here ContainerName
is the user-defined name of the container. In the begin ... end
block the container's field are instead listed. Four possible formats are available:
"field-1" → T1
– field defined as typeT1
with namefield-1
;"field-1" → T2(1.0)
– field defined as typeT2
with namefield-2
;T1(1.0)
– unnamed filed with typeT1
;T2
– unnamed filed with typeT2
;
You can also use a type-only definition of the field or a type constructor. In the latter case, there is the possibility to create an empty constructor for the new container, where all the fields initialization is handled by the container constructor itself.
Container iteration
The possibility to store mixed-types within a single container is end in itself if not comes along with an efficient way to iterate it. Therefore, Containers.jl
containes a set of utility tools which are designed to allocation-freely iterate the user-defined containers. To best introduce their use, let's consider and example where a user defined container Container
is created and a function test_function
is defined as:
function test_function(c::Container)
val = 0.0
for i in eachindex(c)
c[i].x = 1.0i
val += iterate_function(c[i])
end
return val
end
This function modify a (common) field of the container and apply iterate_function
to compute a val
variable, which is returned as output. The main issue here is associated to the fact that, being each element of the container of a different type, this function allocates.
Function barriers method
One possible solution is to exploit function barriers to avoid it at the cost of a very structured and less portable/maintainable code. As additional drawback, poor performances of this approach are observed. For comparison, consider a 2 elements container and a iterate_function
that simply reads a value within a container element:
julia> @benchmark test_barrier($c)
BenchmarkTools.Trial: 10000 samples with 995 evaluations.
Range (min … max): 27.461 ns … 2.720 μs ┊ GC (min … max): 0.00% … 98.28%
Time (median): 29.851 ns ┊ GC (median): 0.00%
Time (mean ± σ): 39.396 ns ± 144.435 ns ┊ GC (mean ± σ): 21.35% ± 5.74%
▆▆█▃
▄████▇▅▅▆▄▃▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂▁▂▂▂▂▂▁▁▂▂▂▂▂▂▂▂▂▂▂▁▂▂▂▂▂▂▂▂▂▁▂ ▃
27.5 ns Histogram: frequency by time 66.8 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
Unwrapped iteration method
Within Containers.jl
and alternative approach is adopted by means of the introduction of two macros: @unwrap
and @iterated
. These have two precise roles:
@unwrap
: decoratefor
loops. It transforms the expression in a specialized, non-allocating one. For example, with reference to thetest_function
thefor
loop can be transformed to:
@unwrap for i in eachindex(c)
c[i].x = 1.0i
val += iterate_function(c[i])
end
@iterated
: decorate afunction
that containesContainer
's iterations. It is used to tranform the part of the function associated to theContainer
. For example, with reference to thetest_function
:
@iterated function test_iterated(c::ExampleContainer{N}) where {N}
val = 0.0
@unwrap for i in 1:N
c[i].x = 1.0i
val += iterate_function(c[i])
end
return val
end
Note that the use of @iterated
comes always with the one of @unwrap
as the latter transforms the for
loop while the former the function itself. Note also that some modifications have been done to the new function:
The function signature is now parametric in
N
. HereN
represent the dimension of the container and is a parameter automatically associated to theAbstractContainer
subtypes during their generation.The
for
loop is still unwrapped with@unwrap
but the iteration is specified using the parametric argumentN
.
These last two modification shall be applied to exploit @iterated
functionalities at the current version of Containers.jl
. This may change in future.
With this approach, just with some simple adjustments to the code, a non-allocating, high-performance version of the function can be obtained resulting in (more than) a order of magnitude speed-up:
julia> @benchmark test_iterated($c)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
Range (min … max): 1.496 ns … 3.989 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 1.508 ns ┊ GC (median): 0.00%
Time (mean ± σ): 1.511 ns ± 0.042 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▁▁▃▄▃ ▅▆▆██▇▇▅▂
▂▃▄▆█████▁█████████▁█▅▄▃▂▂▂▁▁▂▂▁▁▁▁▂▂▂▁▂▂▂▂▃▂▂▂▁▃▃▃▃▃▃▃▃▂ ▄
1.5 ns Histogram: frequency by time 1.55 ns <
Memory estimate: 0 bytes, allocs estimate: 0.