Industrial Data Science
in C# and .NET:
Simple. Fast. Reliable.
 
 

ILNumerics - Technical Computing

Modern High Performance Tools for Technical

Computing and Visualization in Industry and Science

tgt

ILNumerics® Function Rules

Three simple function rules establish an efficient memory management and great performance for your algorithms: 

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: ILNumerics.Array<T>, ILNumerics.Cell and ILNumerics.Logical.

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:

[|In|Out|Ret][Array<T>|Cell|Logical]

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 Array<T> InArray<T> OutArray<T> RetArray<T>
Cell Array Cell InCell OutCell RetCell
Logical Array Logical InLogical OutLogical RetLogical
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 Using ... End Using block is used.

The ILNumerics.Scope.Enter() function creates an artificial scope and provides the following guarantees:

  • 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() function receives all input parameters (i.e. all parameters of type InArray<T>, InCell or InLogical) which may appear in the parameter list. These input parameters will be kept alive during the scope block. In addition to that, local arrays are tracked and released automatically after the block was left.

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 empty array or anything but null), this signals the function to actually perform all computations required to provide the parameter. Otherwise, when an output parameter is null, the function can choose another more efficient way of computation without calculating the corresponding results.

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' from the example below:

  • frequencies[0,full] = ... - use the setter properties to alter parts of the array. All subarray access rules are permitted. Alternatively, the SetRange() 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) the Assign() method is used instead.

By using output parameters in ILNumerics, neither the out nor the ref keyword (C#) are allowed. By convention, providing null as value for an output parameter instructs the function to skip computations for this parameter, if possible. Hence, it is not necessary to declare dummy variables if a certain output parameter is not required.

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.