ILNumerics® Function Rules
Three simple function rules establish an efficient memory management and great performance for your algorithms:
- Array types for function declarations
- using {...} blocks as artificial scopes
- Handling of output parameters
At the end of this article, an example of a computational function in ILNumerics is presented, with some best practices for writing ILNumerics functions.
Array Types for Function Declarations
ILNumerics provides distinct array types, specifying not only the element type, but also lifetime and mutability.
Within a function body all array variables must be of one of these types:
Make sure to explicitly specify the type of all array variables! Don't use 'var' with array variables in C#!
In addition to that, more array types exist which are used only in the signature of a function:
- "Input arrays" are immutable and used as input parameter of a function: InArray<T>, InLogical and InCell.
- "Output arrays" serve as additional, optional, output parameter of a function and act like a proxy to a local array from the calling scope: OutArray<T>, OutLogical and OutCell.
- "Return arrays" are always and only used as return type of a function: RetArray<T>, RetLogical and RetCell.
All array types are easily distinguishable by name. The general naming scheme is as follows:
Let's assume, a function receives two matrices as input parameters and returns a cell array. The function signature might look as follows:
The following table lists all array types existing in ILNumerics and describes their lifetimes:
Content | Local Array | Input Parameter | Output Parameter | Return Value |
---|---|---|---|---|
Dense Array | In |
Out |
Ret |
|
Cell Array | In |
Out |
Ret |
|
Logical Array | In |
Out |
Ret |
|
Lifetimes | ||||
Used in function .. | body | signature | signature | signature |
Disposed automatically ... | when current scope is left | when function scope is left | when target array is freed | after the first use |
Functionality | ||||
Im-/mutability | mutable | immutable | mutable | immutable |
Assignments to variables of that type: | A = ... | inA = ... | outA.a = ... | (no locals) [1] |
[1] Return values are volatile and disposed after the first use.
All array types share the same set of functionality. They provide the same set of member functions for read access and can be used interchangeably. ILNumerics transparently converts between compatible types automatically. The only exception: Input arrays and return arrays are immutable. This means, they lack of any member function or setter property which could be used to alter the content of the array. Also, obviously, these types cannot be used in situations where a mutable array is expected. The compiler will produce an error at compile time when trying to use an array in a wrong context.
using() blocks as artificial scopes
Function bodies of algorithm functions must be enclosed with an artificial scope, assembled out of a using( ) { ... block:
In Visual Basic, a corresponding
The
- All arrays created inside the scope are released immediately when leaving the block: memory for A and B is released immediately and pooled for subsequent reuse.
- Input parameters are released after the block was left: inA and inB.
- If an array style is established with entering a new scope (optional) the old array style is robustly restored.
Therefore, the
Scope.Enter(...,style) accepts an additional, optional parameter determining the array style which is to be used within the scope block. The scoping mechanism ensures the array style value for the code inside the using () { ... } block. When the block is exited the former array style is recovered. Note, that the array style setting affects the current thread only.
Output parameters
Output parameters are used whenever a function must return more than one array. In ILNumerics, output parameters act as a reference or proxy to another (local) array in the calling scope. If a function receives a valid object reference for an output parameter (i.e. an
Modifications and assignments to output parameters immediately alter the underlying source array in the callers scope. In order to make this work, output parameters can be modified in one of the following ways only. Consider an output variable named '
frequencies[0,full] = ... - use the setter properties to alter parts of the array. All subarray access rules are permitted. Alternatively, theSetRange() method may be used if the language used does not support such properties.frequencies.a = ... - reassign a new array to the output parameter. It is important to use the.a setter property. (Again: the compiler is your friend.) For languages not supporting such properties (f.e. Visual Basic .NET) theAssign() method is used instead.
By using output parameters in ILNumerics, neither the
Note, that ILNumerics functions may declare any number of output parameters (OutArray<T>, OutLogical or OutCell) in their function signature. They all belong to the parameter list. The return type, however, must always be declared as one of the special return array types: RetArray<T>, RetLogical or RetCell.
Example of a computational function in ILNumerics®
Note that only local array types are declared in the body - all other types are placed inside the function signature (only there, but nowhere else!) Also note that the parameter list may just as well contain any other parameter useful for the function, next to ILNumerics input and/or output parameters. For example, we could extend the list by, let's say: another double a1 parameter and use this inside the function normally. Also, ILNumerics does not impose any order of parameters nor need the ILNumerics parameters be grouped together. The following function signature is perfectly fine:
Best Practices for Writing ILNumerics® Functions
In contrast to the obligatory rules, the following hints are optional. The user may decide to use them in order to create more readable and convenient code.
- All parameters - where applicable - should be defined as optional parameters (see: C# language reference) at the end of the parameter list. Default value for all array parameters should be
null . This allows the user to pick only those output parameters, which are really required in long parameter lists (using named arguments).
- Input parameters are immutable - their content cannot change. If your algorithm requires an input parameter to be changed, copy it into a local variable and change this instead:
Memory Management Internals
The function rules as defined here were designed to enable a simple, yet efficient memory management. They release you from having to track your memory in your algorithms. Instead, just follow these 3 rules and ILNumerics manages your memory as efficiently as possible.
The memory management does heavily rely on the strong type system in .NET. Arrays in ILNumerics exist as multiple instances of thin 'proxy' classes, all pointing to the same memory. The concrete array type defines lifetime, mutability and element type. While the element type of an array cannot change throughout an algorithm, the lifetime and mutability type of an array will frequently change. Conversions happen automatically (implicitly) when an array is provided as input parameter to a function, when assigning to arrays or when arrays are returned from a function. The conversions happen on the highest layer of the ILNumerics array class hierarchy only.
When an array was converted it was turned into another array instance with different properties. For example, when returning a local array from a function the returned array is volatile: it will allow a single use only but automatically dispose itself off after the first use. Also, such return type arrays are immutable and do not allow to change their memory.
However, the memory used to store the elements of the local array is not copied during the conversion. Instead, the return type array (RetArray<T>) will point to the same memory as the local array. Since such conversions happen all the time in your algorithms they require great efficiency. In version 5 ILNumerics optimizes away even the creation of the wrapper array types. Most conversions now correspond to little more than simple reference counter increments/decrements.
Next to efficiency, another important design aspect was that the user should not have to deal with any subtleties of memory management. In fact, we suggest not to think about type conversions too much. Arrays can have multiple states and multiple arrays can be associated with the same memory. It is often not immediately obvious what happens under the hood, when memory is released and which objects are reused and when. The lesson to take away is: you don't have to know every little aspect of those internals in order for it to work efficiently! Just follow the above rules and let ILNumerics figure out the best way to execute your algorithm automatically!
Don't get confused by the fact that intellisense will show different types for many internal ILNumerics functions! The reason is that many internal functions of ILNumerics don't follow the above rules. Instead, the API enables even more optimizations by exposing specialized (abstract) array types which are not intended to be used directly by algorithm authors. In fact, Intellisense may sometimes suggests that a function expects a certain type of parameter, like 'BaseArray<double>' or ConcreteArray<double,Array<double>,InArray<double>,.....>.
Don't try to create those types yourself! Don't try to manually convert your arrays to those types!
Leave all conversion to the ILNumerics implicit array conversion operators. The compiler will complain if a conversion is not available. Most the time, using your local arrays in any of these situations will work just fine.