-
Notifications
You must be signed in to change notification settings - Fork 8
Design Overview
Matrices and Vectors are modelled using four levels of abstraction. The terminology used in ascending order of abstraction, is:
- ArrayData - Handles memory management, modelling a reference counted array of a primitive type (implemented in storage/arraydata.d).
- Containers - Containers wrap an ArrayData object, providing copy-on-write functionality as well as structured access to the underlying memory; for instance providing dimensionality to matrices or strided access for vectors. The only provided containers are CowArray and CowMatrix (implemented in storage/cowarray.d and storage/cowmatrix.d), modelling two and one-dimensional access respectively, as well as appropriate slicing capabilities.
- Storages - Their role is to model different memory storage types, by providing different mappings of indices to the underlying container. For example, a diagonal matrix implementation (such as the one implemented in storage/diagonalmat.d) may use a CowArray to only store the diagonal elements, which it accesses when the two matrix indices are equal, returning zero otherwise. Also, importantly, the storage type provides the necessary low-level implementations of matrix/vector operations, which are usually delegated to an appropriate BLAS/LAPACK call.
- BasicMatrix/BasicVector - This is the top level which wraps a Storage type. It provides 'decorations' if you will: convenience methods and types, mapping between operators and Storage method calls, default implementations of behaviours which are missing from the wrapped storage type.
There is an important distinction to be made between view storage types and non-view storage types (BasicGeneralMatrixStorage and BasicGeneralMatrixViewStorage, for example). Non-views are simple, concrete types with value semantics, implemented using copy-on-write. Views are a reference type, which point to non-view storages, allowing, for example, a vector-like interface to a column of a matrix, with modifications being propagated to the matrix. The memory used by a matrix gets freed only when the last view pointing to that matrix is freed - there are no 'dangling views'.
To allow both views and copy-on-write behaviour, there exist two ref-counted levels: each storage type (e.g. GeneralMatrixStorage) stores a RefCounted reference to a Container (e.g. RefCounted!(CowMatrix!double) == CowMatrixRef), but the Container itself (CowMatrix!double) stores an ref-counted array (e.g. ArrayData!double) of the element type. These two levels serve two different purposes:
- The first one, RefCounted!(CowMatrix!double) allows a non-view storage type and multiple views to point to the same CowMatrix!double - pointing to the same conceptual matrix. Since there is only one CowMatrix, if one modifies a view, all the other views will be modified as well.
- The second one, the ref-counted array implemented by ArrayData, exists to allow for copy-on-write behaviour: different CowMatrix's can point to the same memory block, even though conceptually they point to different matrices - this happens after a matrix-matrix assignment, for example.
The second level hints to why the first level is needed: if views simply stored a CowMatrix (instead of a RefCounted!CowMatrix), even though different views could point to the same memory (by sharing an ArrayData), when one of them got changed, the copy-on-write behaviour would kick in, and the view which got modified would be made to point to a newly allocated memory area, leaving the other views unchanged.
In essence, this allows for two conceptually different kinds of memory sharing between storages:
- Store references to the same CowMatrix. This is a 'view' behaviour, it means that the two storages refer to the same matrix. This is why the view Storage types, do not have the postblit c-tor: the RefCounted member postblit creates this behaviour by default.
- Store references to different CowMatrix's that point to the same array in memory. This is the only way non-view storage types can share memory, allowing copy-on-write behaviour. Conceptually, the two storages `just happen' to have equal values so they might as well share the memory. When one of them gets modified, new memory is allocated, and a copy is performed.
Let L1 refer to the reference counter inside Storage types and L2 refer to the counter inside ArrayData, then:
- The L1 refcount keeps track of how many views there are of a given matrix/vector object;
- Copying a view increments the L1 refcount. Destroying a view decrements the L1 refcount;
- The L2 refcount keeps track of how many matrix/vector (not view) objects are pointing to the same memory;
- Destroying the last view of a matrix/vector and the matrix/vector object itself decrements the L2 refcount;
- Modifying the contents of a matrix/vector or view causes copying of the underlying storage if the L2 refcount is greater than 1;
- When the L2 refcount hits zero, the memory holding the matrix/vector data is freed.