![]() |
Blaze 3.9
|
Matrix adapter for upper unitriangular matrices.
More...
#include <BaseTemplate.h>
Matrix adapter for upper unitriangular matrices.
The UniUpperMatrix class template is an adapter for existing dense and sparse matrix types. It inherits the properties and the interface of the given matrix type MT and extends it by enforcing the additional invariant that all diagonal matrix elements are 1 and all matrix elements below the diagonal are 0 (upper unitriangular matrix). The type of the adapted matrix can be specified via the first template parameter:
The following examples give an impression of several possible upper unitriangular matrices:
The storage order of an upper unitriangular matrix is depending on the storage order of the adapted matrix type MT. In case the adapted matrix is stored in a row-wise fashion (i.e. is specified as blaze::rowMajor), the uniupper matrix will also be a row-major matrix. Otherwise, if the adapted matrix is column-major (i.e. is specified as blaze::columnMajor), the uniupper matrix will also be a column-major matrix.
An upper unitriangular matrix is used exactly like a matrix of the underlying, adapted matrix type MT. It also provides (nearly) the same interface as the underlying matrix type. However, there are some important exceptions resulting from the upper unitriangular matrix constraint:
In case a resizable matrix is used (as for instance blaze::HybridMatrix, blaze::DynamicMatrix, or blaze::CompressedMatrix), this means that the according constructors, the resize()
and the extend()
functions only expect a single parameter, which specifies both the number of rows and columns, instead of two (one for the number of rows and one for the number of columns):
In case a matrix with a fixed size is used (as for instance blaze::StaticMatrix), the number of rows and number of columns must be specified equally:
The diagonal elements of an upper unitriangular matrix are fixed to 1. This property has two implications. First, that means that the diagonal elements of a newly created uniupper matrix are pre-initialized to 1:
Second, this means that it is only allowed to modify elements in the upper part of the matrix, but not the diagonal elements and not the elements in the lower part of the matrix. Also, it is only possible to assign matrices that are upper unitriangular matrices themselves:
The upper unitriangular matrix property is also enforced for uniupper custom matrices: In case the given array of elements does not represent an uniupper matrix, a std::invalid_argument exception is thrown:
Finally, the upper unitriangular matrix property is enforced for views (rows, columns, submatrices, ...) on the uniupper matrix. The following example demonstrates that modifying the elements of an entire row and submatrix of an uniupper matrix only affects the upper matrix elements:
The next example demonstrates the (compound) assignment to rows/columns and submatrices of uniupper matrices. Since only upper elements may be modified the matrix to be assigned must be structured such that the upper unitriangular matrix invariant of the uniupper matrix is preserved. Otherwise a std::invalid_argument exception is thrown:
Although this results in a small loss of efficiency during the creation of a dense uniupper matrix this initialization is important since otherwise the upper unitriangular matrix property of dense uniupper matrices would not be guaranteed:
It is important to note that dense upper unitriangular matrices store all elements, including the elements in the lower part of the matrix, and therefore don't provide any kind of memory reduction! There are two main reasons for this: First, storing also the lower elements guarantees maximum performance for many algorithms that perform vectorized operations on the uniupper matrix, which is especially true for small dense matrices. Second, conceptually the UniUpperMatrix adaptor merely restricts the interface to the matrix type MT and does not change the data layout or the underlying matrix type.
Since the diagonal elements have a fixed value of 1 it is not possible to self-scale an uniupper matrix:
An UniUpperMatrix matrix can participate in numerical operations in any way any other dense or sparse matrix can participate. It can also be combined with any other dense or sparse vector or matrix. The following code example gives an impression of the use of UniUpperMatrix within arithmetic operations:
Note that it is possible to assign any kind of matrix to an uniupper matrix. In case the matrix to be assigned is not uniupper at compile time, a runtime check is performed.
The Blaze library tries to exploit the properties of upper (uni-)triangular matrices whenever and wherever possible. Thus using an upper (uni-)triangular matrix instead of a general matrix can result in a considerable performance improvement. However, there are also situations when using an (uni-)upper matrix introduces some overhead. The following examples demonstrate several common situations where (uni-)upper matrices can positively or negatively impact performance.
When multiplying two matrices, at least one of which is upper (uni-)triangular, Blaze can exploit the fact that the lower part of the matrix contains only default elements and restrict the algorithm to the upper and diagonal elements. The following example demonstrates this by means of a dense matrix/dense matrix multiplication:
In comparison to a general matrix multiplication, the performance advantage is significant, especially for large matrices. Therefore is it highly recommended to use the UniUpperMatrix adaptor when a matrix is known to be upper unitriangular. Note however that the performance advantage is most pronounced for dense matrices and much less so for sparse matrices.
A similar performance improvement can be gained when using an upper (uni-)triangular matrix in a matrix/vector multiplication:
In this example, Blaze also exploits the structure of the matrix and approx. halves the runtime of the multiplication. Also in case of matrix/vector multiplications the performance improvement is most pronounced for dense matrices and much less so for sparse matrices.
In contrast to using an upper (uni-)triangular matrix on the right-hand side of an assignment (i.e. for read access), which introduces absolutely no performance penalty, using an (uni-)upper matrix on the left-hand side of an assignment (i.e. for write access) may introduce additional overhead when it is assigned a general matrix, which is not upper (uni-)triangular at compile time:
When assigning a general, potentially not uniupper matrix to another uniupper matrix it is necessary to check whether the matrix is uniupper at runtime in order to guarantee the upper unitriangular property of the uniupper matrix. In case it turns out to be upper unitriangular, it is assigned as efficiently as possible, if it is not, an exception is thrown. In order to prevent this runtime overhead it is therefore generally advisable to assign uniupper matrices to other uniupper matrices.
In this context it is especially noteworthy that the multiplication of two upper unitriangular matrices always results in another uniupper matrix: