表达式模板(expression templates)
Blitz++是一个高性能数组算术程序库,开拓性的使用了explicit metaprograming。
如果将其所解决的问题归结为一句话,那就是:数组算术的幼稚实现,对于任何有意义的计算来说,是可怕低效的。为了弄明白这句话所表达的意思,看看下面几条无趣的语句:
x = a + b + c;
其中x, a, b ,c 都是二维数组。数组加法运算符的规范实现是:
template
Matrix<_Ty> operator + (Matrix<_Ty> const & a, Matrix<_Ty> const & b)
{
Matrix<_Ty> result(n, m);
…
return result;
}
这里就有两个对象被创建了,一个是result,一个是函数返回值的临时对象,Effective C++条告诉我们:
条款23: 必须返回一个对象时不要试图返回一个引用,这里就是典型的例子。当然现代编译器所做的比我们想象的要多很多,
对于result这类具名的返回值,在编译器优化阶段能够被侦测,然后与返回值临时对象合并(NVRO),然而在
x = a + b + c;
这一表达式中,产生的临时对象依然会有两个,就是a+b的结果t1,t1+c的结果t2; t2 + c 的结果t2,在高性能计算
领域,临时对象一直是C/C++ 被诟病的主要原因之一。在函数式程序设计语言中往往有着 惰性求值这一特性,即只有当表达式确实需要时才对其求值。=
在表达式解析树(parse tree)中,评估始于叶子节点并向上推进直至根节点。如何才能至上向下的管理表达式计算成为了问题的关键。
下图为x = a + b + c 的解析树。
当然这不是一个一般的表达式,矩阵(二维数组)表达式涉及了其他的一些操作,比如乘法,这些操作需要他们自己的评估策略,并且表达式可能是任意大并且是嵌套的,如果采用指针和节点构建的解析树,不得不在运行期遍历解析树,这势必对性能造成影响。由此表达式模版应运而生。
其实作手法还是相当直接的,临时变量来源于不必要的求值,那就不求值就行了,让operator+不返回计算出来的结果,而仅仅是将实参的引用打包到一个Expression实体中,并贴以标签:
// 操作标签
struct plus; struct minus;
// 表达式树节点
template
struct Expression
// 加法运算符
template
Expression operator+(L const& l, R const& r)
{ return Expression(l, r); }
当我们写下a+b时,并没有足够的信息执行计算,它被编码为Expression,数据通过表达式所存储的引用进行访问。当写下a+b+c时,得到一个如下类型的结果
Expression, plus, Matrix>
但这一切究竟是怎样工作的,让我们看看一个矩阵的加法如何操作的
//加法标签实现逐元素的计算
// 操作标签
struct plus
{
template
static _Ty apply(_Ty a, _Ty b) { return a + b; }
};
然后给表达式Expression一个索引操作
template
struct Expression
{
Expression(_Tl const& lhs, _Tr const& rhs)
: _Lexpr(lhs), _Rexpr(rhs)
{}
value_type operator() (size_type idx, size_type idy) const
{ return OpTag::apply(_Lexpr(idx,idy), _Rexpr(idx, idy));}
_Tl const& _Lexpr;
_Tr const& _Rexpr;
};
这看起来非常简单,然而令人惊讶的是,现在获得了完整的缓式表达式评估(lazy expression evaluation)功能。
注意到Expression的构造函数除了保存引用之外,其实什么也没做。只有执行索引操作时才会评估值,比如表达式a + b,类型是Expression
(a+b)(0,0) == plus::apply(a(0,0) + b(0,0))
== a(0,0) + b(0,0)
同样(a+b+c)[0,0]也在不产生任何临时矩阵的情况下被计算出来:
(a+b+c)(0,0) == plus::apply((a+b)(0,0) , c(0,0))
== plus::apply(plus::apply(a(0,0), b(0,0)), c(0,0))
== plus::apply(a(0,0) + b(0,0), c(0,0))
== a(0,0) + b(0,0) + c(0,0)
现在剩下的就是去实现Matrix赋值运算符了,我们可以访问结果Expression的任何单个元素,二不必去创建临时的Matrix,因此可以通过访问表达式中的每一个元素来计算整个结果:
template
Matrix& operator=(Expr const & expr)
{
size_type n = Row(), m = Col();
for (size_type i = 0; i != n; ++i)
for (size_type j = 0; j != m; ++j)
(*this)(i, j) = expr(i, j);
return *this;
}
没有任何东西是完美无缺的,表达式模版的缺点之一是它们鼓励编写复杂的大型表达式,因为评估被延迟到了赋值运算符调用时,如果程序员希望复用某写中间结果,而早期又没有评估它,她就被迫声明一个类似如下(甚至更糟)的复杂类型:
Expression<
Expression
, plus
, Expression
> temp = a + b + (c – d);
这个类型不但精确而且累赘的反映了计算结构,而且很打击人的信心。通常的迂回解决方式是使用类型擦出来捕获表达式,当然这种情况下会付出相应的代价(动态分派)。
当然这已经不是问题了,C++0x把关键字auto变废为宝,以便在形形色色的声明中进行类型推导,从而我们只需写下:
auto temp = a + b + (c – d);
更多auto的来源及特性见N1705=04-0145 Decltype and auto。