Skip to content
cristicbz edited this page Dec 19, 2011 · 2 revisions

Levels of abstraction

Matrices and Vectors are modelled using four levels of abstraction. The terminology used in ascending order of abstraction, is:

  1. ArrayData - Handles memory management, modelling a reference counted array of a primitive type (implemented in storage/arraydata.d).
  2. 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.
  3. 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.
  4. 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.

View and non-view storage types

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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.
Clone this wiki locally