In last post I talked about the ongoing refactor of linalg. So we are going to have untemplated vector and matrix types, using a void pointer and checking type information in runtime. Say you have some vectors and do some computations like
Vector a, b, c;
c = add(add(a, b), c);
A simple idea is to switch over element types of vectors and call templated routine linalg::add<T>
.
In this way we will have to switch every time, over each expression involved, e.g, a, b, c and a+b.
This is not desirable not only for runtime overhead but also much boilerplate code we will have to add.
To prevent redundant big switches every time, lazy evaluation is a quite nice idea here. Let’s start with a simple Vector
:
class Vector {
public:
Vector(void* data, EPrimitiveType ptype);
operator SGVector<T>();
private:
EPrimitiveType ptype;
void* data;
};
It has templated conversion operator, which allows conversion to SGVector<T>
that shares the underlying memory.
To support lazy evaluation, we define expression types to represent the expression to be evaluated such that a expression tree consisting of multiple expressions can be evaluated at one time.
template <typename E>
class Exp {
E& self() { return *static_cast<E*>(this) }
const E& self() const { return *static_cast<const E*>(this); }
}
Note that Exp
is a templated class since we are using CRTP pattern to support static polymorphism.
One of the expression types we have is VectorExp
. By Vector
it means that it evaluates to some Vector
. Similarly we can have MatrixExp
and so on. Then we can derive specific types of vector expressions., for example BinaryVectorExp
is a template class that represents some binary expressions with a custom operator.
template <typename E>
class VectorExp: public Exp<VectorExp<E>>;
template <
typename OP,
typename E1,
typename E2
>
class BinaryVectorExp: public VectorExp<BinaryVectorExp<OP, E1, E2>>;
Here OP
is an operator class that defines the templated evaluation process. For example, vector addition can be defined like:
struct VectorAdd {
VectorAdd(double alpha, double beta): alpha(alpha), beta(beta) { }
template<T>
SGVector<T> apply (
const SGVector<T>& a,
const SGVector<T>& b
) {
return linalg::add(a, b, alpha, beta);
}
double alpha, beta;
};
Now we can put things to an expression. An expression may contain simply a vector, or one or more expressions and an operator. The problem we have now is how we put a vector to an expression since it is an expression actually. This is important since it is reasonable that add
can accept both Vector
and VectorExp
. We define the expression type, VectorRefExp
. By wrapping a Vector
instance into a VectorRefExp
, we are able to use vector as other expression types when writing an expression.
class VectorRefExp: public VectorExp<VectorRefExp>
{
private:
Vector vector;
};
The last step for this is to define a wrapper method can accept either Vector
or VectorExp
. Let’s continue with the vector addition example above.
template <
typename E1,
typename E2
>
auto add_impl(VectorExp<E1> e1, VectorExp<E2> e2, double alpha, double beta)
{
return BinaryVectorExp(VectorAdd(alpha, beta), e1.self(), e2.self());
}
template <typename... Args>
auto add(Args&& args...)
{
return add_impl(forward_exp<Args>(args)...);
}
In this example, arguments are forwarded to add_impl
and cast to VectorRefExp
whenever Vector
is passed by forward_exp
.
Now we could write down our lazy expression like:
Vector a, b;
// do some initialization
auto c = add(a, b); // c is an intermediate expression
Vector d = add(c, a); // implicitly trigger evaluation when assigning to a vector
How do we evaluate such an expression? Let’s talk about it in the next post.
>> Home