Expression Templates
Preface
在研究 CRTP 时发现了一种经典应用——表达式模板。今天学一学,顺便水一篇博客。🐶
Motivation and example
表达式模板是一种模板元编程,它在编译期展开向量的一系列复合运算,从而将辅助空间从 $O(n)$ 降到 $O(1)$。相比于朴素算法,表达式模板属于惰性求值,求值时间点是赋值运算。
举个例子,有这样一个 vector 运算:
1 | tot = (a + b) * c; |
朴素算法的语义是:
1 | { |
即使你知道「引用限定成员函数」,使你可以抹掉 tmp1
的空间分配,但是 tmp0
的辅助空间总是逃不掉。
能帮助你的只有表达式模板,它可以表达出这样的语义:
1 | for (size_t i=0, n=a.size(); i<n; ++i) { |
如此一来需要的辅助空间就是寄存器级别的!去掉了函数调用也更方便编译器做进一步的优化。
My first attempt
我的首次尝试
忍住不看教程,我先试着实现出来,看看差距有多大。🤔
我的想法:
- 利用类型系统,将懒惰信息记录在模板参数中
- 定义惰性二元运算符,它的操作数可以是 vector 或者惰性二元运算符。
- 魔改
vector
,在赋值运算符中触发求值;重载加减乘除运算。
Draft 1
1 |
|
What's wrong
这段程序是正确的!😁但也有一些犯蠢的地方:
- 没有重载运算符,用起来太丑;
- 显式模板参数太多,比如
int
出现太多次。理想中应该只在Vec
定义时表明T = int
,后面的通通自动推导; - 魔改的
Vec
类没有沿用std::vector
的非默认构造函数; - 当调用
vec = vec
时会错误地调用惰性求值版本; - 没有显式 CRTP,证明不够熟练;
- 在计算前调用
vector::resize()
真的好吗?有点违反零开销抽象的感觉。
The second attempt
查文档再改进
针对上一章节列出的缺点,我查阅文档做了一些改进:
Inheriting constructors
1 | template<typename T, class Container = std::vector<T>> |
这样就能使用 std::vector
的构造接口了,我们的 C++ 真的太好用啦!
Template Inner Class
我们可以将 DeferredOperator
类定义在 Vec
内部,这样就不用到处传递基础数据类型啦!
Who needs operator+()?
Vec
类需要实现operator+()
,并由这个函数返回一个DeferredOperator
DeferredOperator
类需要实现operator+()
,这个函数同样返回一个DeferredOperator
Ambiguity of operator=()
为了区分 Vec = Vec
和 Vec = DeferredOperator
,我们需要更精细地定义参数。
Draft 2
1 |
|
Direction of improvement
WoW! 这段代码也是正确的!但还是写得有点臭:
- 这里只写了重载加法。如果每种运算符都要写两个成员函数,那也太难看了。
- 只对
Vec
类实现了惰性加法。万一再来一个Matrix
类呢?惰性运算能不能接口化? - CRTP 在哪?
Peak of Evolution
这次依然是原创代码,改进了一些地方:
Class Diagram
多层次的 CRTP 用起来容易头晕,应该事先设计好类的关系:
橙色和红色的类才是 CRTP 的模板实参。白色的类主要用作接口,类似于抽象类。
ET interface
- 用户类只要继承
VectorExpresstion
接口,同时定义合适的operator[]
和operator=
即可实现模板表达式。 - 用户可以在
operator=
中做性能优化,例如手工循环展开、SIMD 等等,非常灵活。 - 用户还可以自定义运算符,但我暂时想不到有什么不啰嗦的实现方法(除了宏)。
Code
1 | // necessary header for Expression Template library |
Conclusion and Prospect
一个基本可用的表达式模板库诞生了。但是要记住:
- 表达式模板离开了内联优化就是负优化。经过简单测试,gcc 至少要在
-O1
条件下才有内联成员函数的能力。 - 使用表达式模板和其他优化手段并不冲突,可以在自定义
operator=
处做 SIMD,循环展开等等。
我不得不承认这份代码对比基础库的水准还是差了很多,至少有以下方面可以改进:
- more type trait,描述表达式的可加性、可乘性、可被除和可除性等等。
- 进一步简化用户自定义运算符的代码。
- 实现一元运算符。
- 在矢量和标量的混合运算中抹去显式构造标量。
但模板元编程暂时不是我的主要学习方向,所以还是再等等吧~