Custom subclasses
Hi Klaus,
I did try to subclass a Blaze type in a naive way. My motivation was to use a custom memory allocation, I used the CustomMatrix
class. A subclass seemed to be an attractive choice in comparison to managing separate smart pointer objects.
I suppose this is not expected to work by design? Is there a simple way to achieve this without duplicating a lot of code? When searching how this is done in similar libraries I found this document for Eigen.
Current workaround is to overload the dereference operator to get the Blaze type back.
Kind regards
Jannik
Comments (6)
-
-
reporter Hi Klaus,
thank you for this answer I did look closer what was the problem in my case. If I use the subclass then some type traits from Blaze are no longer working, which I use in my code. I first thought this would also break things from Blaze, but everything seems to be fine. An example which is similar to my code
template <typename M> std::enable_if<IsDenseMatrix<M>::value && IsRowMajorMatrix<M>::value>::type func(M& m) { // ... const int rows = m.rows(); // ... }
will not compile because
IsDenseMatrix<M>::value
will be false for the subclass. Questionable if these traits are part of the API, so I now think I have to blame myself.And you are right the proposal is not clear. The proposal was to make subclassing possible (it is), or if it is possible but not straightforward (it is), then create a page like the linked one to document how.
Kind regards
Jannik
-
Hi Jannik!
Thanks for the feedback. I will consider this issue as if the title was "Create wiki page for the customization of types and operations". Also, you are correct that the mentioned type traits stop working as they are implemented too type specific. I will fix this as quickly as possible. For now, please just provide the correct specializations manually. For instance:
template< typename Type, bool SO > struct IsDenseMatrix< MyCustomMatrix<Type,SO> > : public BoolConstant<true> {}; template< typename Type > struct IsRowMajorMatrix< MyCustomMatrix<Type,rowMajor> > : public BoolConstant<true> {};
Best regards,
Klaus!
-
-
assigned issue to
-
assigned issue to
-
- changed status to open
-
- changed status to resolved
Summary
The tutorial and the wiki have been extended by a page for customizing existing vector and matrix classes. The updated tutorial is immediately available via cloning the Blaze repository. The wiki will be updated with the official release of Blaze 3.2.
Custom Data Members
So far the Blaze library does not provide a lot of flexibility to customize the data members of existing vector types and matrix types. However, to some extend it is possible to customize vectors and matrices by inheritance. The following example gives an impression on how to create a simple variation of
CustomMatrix
, which automatically takes care of acquiring and releasing custom memory.template< typename Type // Data type of the matrix , bool SO = defaultStorageOrder > // Storage order class MyCustomMatrix : public CustomMatrix< Type, unaligned, unpadded, SO > { public: explicit inline MyCustomMatrix( size_t m, size_t n ) : CustomMatrix<Type,unaligned,unpadded,SO>() , array_( new Type[m*n] ) { this->reset( array_.get(), m, n ); } private: std::unique_ptr<Type[]> array_; };
Please note that this is a simplified example with the intent to show the general approach. The number of constructors, the memory acquisition, and the kind of memory management can of course be adapted to specific requirements. Also, please note that since none of the Blaze vectors and matrices have virtual destructors polymorphic destruction cannot be used.
Custom Operations
There are two approaches to extend Blaze with custom operations. First, the
map()
functions provide the possibility to execute componentwise custom operations on vectors and matrices. Second, it is possible to add customized free functions.The map() Functions
Via the unary and binary
map()
functions it is possible to execute componentwise custom operations on vectors and matrices. The unarymap()
function can be used to apply a custom operation on each single element of a dense vector or matrix or each non-zero element of a sparse vector or matrix. For instance, the following example demonstrates a custom square root computation on a dense matrix:blaze::DynamicMatrix<double> A, B; B = map( A, []( double d ) { return std::sqrt( d ); } );
The binary
map()
function can be used to apply an operation pairwise to the elements of two dense vectors or two dense matrices. The following example demonstrates the merging of two matrices of double precision values into a matrix of double precision complex numbers:blaze::DynamicMatrix<double> real{ { 2.1, -4.2 }, { 1.0, 0.6 } }; blaze::DynamicMatrix<double> imag{ { 0.3, 1.4 }, { 2.9, -3.4 } }; blaze::DynamicMatrix< complex<double> > cplx; // Creating the matrix // ( (-2.1, 0.3) (-4.2, -1.4) ) // ( ( 1.0, 2.9) ( 0.6, -3.4) ) cplx = map( real, imag, []( double r, double i ){ return complex( r, i ); } );
These examples demonstrate the most convenient way of defining a unary custom operation by passing a lambda to the
map()
function. Alternatively, it is possible to pass a custom functor:struct Sqrt { double operator()( double a ) const { return std::sqrt( a ); } }; B = map( A, Sqrt() );
In order for the functor to work in a call to
map()
it must define a function call operator, which accepts arguments of the type of the according vector or matrix elements.Although the operation is automatically parallelized depending on the size of the vector or matrix, no automatic vectorization is possible. In order to enable vectorization, a
load()
function can be added to the functor, which handles the vectorized computation. Depending on the data type this function is passed one of the following Blaze SIMD data types:- SIMD data types for fundamental data types
- blaze::SIMDint8: Packed SIMD type for 8-bit signed integral data types
- blaze::SIMDuint8: Packed SIMD type for 8-bit unsigned integral data types
- blaze::SIMDint16: Packed SIMD type for 16-bit signed integral data types
- blaze::SIMDuint16: Packed SIMD type for 16-bit unsigned integral data types
- blaze::SIMDint32: Packed SIMD type for 32-bit signed integral data types
- blaze::SIMDuint32: Packed SIMD type for 32-bit unsigned integral data types
- blaze::SIMDint64: Packed SIMD type for 64-bit signed integral data types
- blaze::SIMDuint64: Packed SIMD type for 64-bit unsigned integral data types
- blaze::SIMDfloat: Packed SIMD type for single precision floating point data
- blaze::SIMDdouble: Packed SIMD type for double precision floating point data
- SIMD data types for complex data types
- blaze::SIMDcint8: Packed SIMD type for complex 8-bit signed integral data types
- blaze::SIMDcuint8: Packed SIMD type for complex 8-bit unsigned integral data types
- blaze::SIMDcint16: Packed SIMD type for complex 16-bit signed integral data types
- blaze::SIMDcuint16: Packed SIMD type for complex 16-bit unsigned integral data types
- blaze::SIMDcint32: Packed SIMD type for complex 32-bit signed integral data types
- blaze::SIMDcuint32: Packed SIMD type for complex 32-bit unsigned integral data types
- blaze::SIMDcint64: Packed SIMD type for complex 64-bit signed integral data types
- blaze::SIMDcuint64: Packed SIMD type for complex 64-bit unsigned integral data types
- blaze::SIMDcfloat: Packed SIMD type for complex single precision floating point data
- blaze::SIMDcdouble: Packed SIMD type for complex double precision floating point data
All SIMD types provide the
value
data member for a direct access to the underlying intrinsic data element. In the following example, this intrinsic element is passed to the AVX function_mm256_sqrt_pd()
:struct Sqrt { double operator()( double a ) const { return std::sqrt( a ); } SIMDdouble load( const SIMDdouble& a ) const { return _mm256_sqrt_pd( a.value ); } };
In this example, whenever vectorization is generally applicable, the
load()
function is called instead of the function call operator for as long as the number of remaining elements is larger-or-equal to the width of the packed SIMD type. In all other cases (which also includes peel-off and remainder loops) the scalar operation is used.Please note that this example has two drawbacks: First, it will only compile in case the intrinsic
_mm256_sqrt_pd()
function is available (i.e. when AVX is active). Second, the availability of AVX is not taken into account. The first drawback can be alleviated by making theload()
function a function template. The second drawback can be dealt with by adding asimdEnabled()
function template to the functor:struct Sqrt { double operator()( double a ) const { return std::sqrt( a ); } template< typename T > T load( const T& a ) const { return _mm256_sqrt_pd( a.value ); } template< typename T > static constexpr bool simdEnabled() { #if defined(__AVX__) return true; #else return false; #endif } };
The
simdEnabled()
function must be astatic
,constexpr
function and must return whether or not vectorization is available for the given data typeT
. In case the function returnstrue
, theload()
function is used for a vectorized evaluation, in case the function returnsfalse
,load()
is not called.Note that this is a simplified example that is only working when used for dense vectors and matrices with double precision floating point elements. The following code shows the complete implementation of the according functor that is used within the Blaze library. The Blaze
Sqrt
functor is working for all data types that are providing a square root operation:namespace blaze { struct Sqrt { template< typename T > BLAZE_ALWAYS_INLINE auto operator()( const T& a ) const { return sqrt( a ); } template< typename T > static constexpr bool simdEnabled() { return HasSIMDSqrt<T>::value; } template< typename T > BLAZE_ALWAYS_INLINE auto load( const T& a ) const { BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T ); return sqrt( a ); } }; } // namespace blaze
The same approach can be taken for binary custom operations. The following code demonstrates the
Min
functor of the Blaze library, which is working for all data types that provide amin()
operation:struct Min { explicit inline Min() {} template< typename T1, typename T2 > BLAZE_ALWAYS_INLINE decltype(auto) operator()( const T1& a, const T2& b ) const { return min( a, b ); } template< typename T1, typename T2 > static constexpr bool simdEnabled() { return HasSIMDMin<T1,T2>::value; } template< typename T1, typename T2 > BLAZE_ALWAYS_INLINE decltype(auto) load( const T1& a, const T2& b ) const { BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T1 ); BLAZE_CONSTRAINT_MUST_BE_SIMD_PACK( T2 ); return min( a, b ); } };
For more information on the available Blaze SIMD data types and functions, please see the SIMD module in the complete Blaze documentation.
Free Functions
In order to extend Blaze with new functionality it is possible to add free functions. Free functions can be used either as wrappers around calls to the
map()
function or to implement general, non-componentwise operations. The following two examples will demonstrate both ideas.The first example shows the
setToZero()
function, which resets a sparse matrix to zero without affecting the sparsity pattern. It is implemented as a convenience wrapper around themap()
function:template< typename MT // Type of the sparse matrix , bool SO > // Storage order void setToZero( blaze::SparseMatrix<MT,SO>& mat ) { (~mat) = blaze::map( ~mat, []( int ){ return 0; } ); }
The
blaze::SparseMatrix
class template is the base class for all kinds of sparse matrices and provides an abstraction from the actual typeMT
of the sparse matrix. However, due to the Curiously Recurring Template Pattern (CRTP) it also enables a conversion back to the actual type. This downcast is performed via the tilde operator (i.e.operator~()
). The template parameterSO
represents the storage order (blaze::rowMajor
orblaze::columnMajor
) of the matrix.The second example shows the
countZeros()
function, which counts the number of values, which are exactly zero, in a dense, row-major matrix:template< typename MT > size_t countZeros( blaze::DenseMatrix<MT,rowMajor>& mat ) { const size_t M( (~mat).rows() ); const size_t N( (~mat).columns() ); size_t count( 0UL ); for( size_t i=0UL; i<M; ++i ) { for( size_t j=0UL; j<N; ++j ) { if( blaze::isDefault<strict>( (~mat)(i,j) ) ) ++count; } } return count; }
The
blaze::DenseMatrix
class template is the base class for all kinds of dense matrices. Again, it is possible to perform the conversion to the actual type via the tilde operator.The following two listings show the declarations of all vector and matrix base classes, which can be used for custom free functions:
template< typename VT // Concrete type of the dense or sparse vector , bool TF > // Transpose flag (blaze::columnVector or blaze::rowVector) class Vector; template< typename VT // Concrete type of the dense vector , bool TF > // Transpose flag (blaze::columnVector or blaze::rowVector) class DenseVector; template< typename VT // Concrete type of the sparse vector , bool TF > // Transpose flag (blaze::columnVector or blaze::rowVector) class SparseVector;
template< typename MT // Concrete type of the dense or sparse matrix , bool SO > // Storage order (blaze::rowMajor or blaze::columnMajor) class Matrix; template< typename MT // Concrete type of the dense matrix , bool SO > // Storage order (blaze::rowMajor or blaze::columnMajor) class DenseMatrix; template< typename MT // Concrete type of the sparse matrix , bool SO > // Storage order (blaze::rowMajor or blaze::columnMajor) class SparseMatrix;
Custom Data Types
The Blaze library tries hard to make the use of custom data types as convenient, easy and intuitive as possible. However, unfortunately it is not possible to meet the requirements of all possible data types. Thus it might be necessary to provide Blaze with some additional information about the data type. The following sections give an overview of the necessary steps to enable the use of the hypothetical custom data type
custom::double_t
for vector and matrix operations. For example:blaze::DynamicVector<custom::double_t> a, b, c; // ... Resizing and initialization c = a + b;
The Blaze library assumes that the
custom::double_t
data type providesoperator+()
for additions,operator-()
for subtractions,operator*()
for multiplications andoperator/()
for divisions. If any of these functions is missing it is necessary to implement the operator to perform the according operation. For this example we assume that the custom data type provides the four following functions instead of operators:namespace custom { double_t add ( const double_t& a, const double_t b ); double_t sub ( const double_t& a, const double_t b ); double_t mult( const double_t& a, const double_t b ); double_t div ( const double_t& a, const double_t b ); } // namespace custom
The following implementations will satisfy the requirements of the Blaze library:
inline custom::double_t operator+( const custom::double_t& a, const custom::double_t& b ) { return add( a, b ); } inline custom::double_t operator-( const custom::double_t& a, const custom::double_t& b ) { return sub( a, b ); } inline custom::double_t operator*( const custom::double_t& a, const custom::double_t& b ) { return mult( a, b ); } inline custom::double_t operator/( const custom::double_t& a, const custom::double_t& b ) { return div( a, b ); }
Blaze will use all the information provided with these functions (for instance the return type) to properly handle the operations. In the rare case that the return type cannot be automatically determined from the operator it might be additionally necessary to provide a specialization of the following four Blaze class templates:
namespace blaze { template<> struct AddTrait<custom::double_t,custom::double_t> { typedef custom::double_t Type; }; template<> struct SubTrait<custom::double_t,custom::double_t> { typedef custom::double_t Type; }; template<> struct MultTrait<custom::double_t,custom::double_t> { typedef custom::double_t Type; }; template<> struct DivTrait<custom::double_t,custom::double_t> { typedef custom::double_t Type; }; } // namespace blaze
The same steps are necessary if several custom data types need to be combined (as for instance
custom::double_t
andcustom::float_t)
. Note that in this case both permutations need to be taken into account:custom::double_t operator+( const custom::double_t& a, const custom::float_t& b ); custom::double_t operator+( const custom::float_t& a, const custom::double_t& b ); // ...
Please note that only built-in data types apply for vectorization and thus custom data types cannot achieve maximum performance!
- Log in to comment
Hi Jannik!
Thanks for creating this proposal. Unfortunately you don't mention what is not working in the description, which leaves me wondering whether you propose a code modification or to add some guidelines similar to the linked webpage to the wiki and tutorial.
Assuming that you need some guidelines, here is a first attempt to give some help. Below you will find a subclass based on
CustomVector
, which should work well for most purposes:This approach should only be used if you want to add data members. If you want to add functionality, I recommend to add a free function as Blaze favors the free function approach (the reasons will be explained in the talk "Free Your Functions!" at CppCon 2017 and Meeting C++ 2017). Issue #87 and #72 might give you some idea how to proceed in that case.
I hope this gives you some idea how to get started.
Best regards,
Klaus!