CTTCG 27 Expression Templates

表达式模板解决了什么问题

表达式模板主要用在大数组的计算上。以相加为例,应该支持的操作有:同长度的数组相加、数组和标量的相加。有时候表达式比较复杂,会涉及多个操作,这种场景下,手工编写代码效率高,但是需要自己控制循环;使用模板编写代码则需要想办法把操作一次性完成,避免多次内存分配和内存访问。

标准库中的 std::valarray 就是想要解决这一类问题,但是其实现因为历史原因可能算不上高效。

例子:

Array<double> x(1000), y(1000);
...
x = 1.2*x + x*y;

如何设计上面的代码?

反面例子:

tmp1 = 1.2*x;     // loop of 1,000 operations
                  // plus creation and destruction of tmp1
tmp2 = x*y;       // loop of 1,000 operations
                  // plus creation and destruction of tmp2
tmp3 = tmp1+tmp2; // loop of 1,000 operations
                  // plus creation and destruction of tmp3
x = tmp3;         // 1,000 read operations and 1,000 write operations

这种模式把几个操作符的计算分开,不仅每次都要分配大量内存,而且还需要访存多趟(趟:pass,指对所有元素遍历一次)。

解决方案:使用代理类模板(本章节说的表达式模板),对表达式的下标运算符重载,使得嵌套后外层的下标访问能够实际指代一个对应位置的表达式。表达式模板本身不存储数据,而且对对应位置也只需要一趟访存,因而效率很高。对于上面的例子,表达式模板的结构可能是这样的:

其中 Array 模板有两个参数,分别是元素类型 T 和表示类型 Rep。通过 Rep 参数,我们可以把表达式模板(本身不存储值)和数组模板统一在一起定义操作。

表达式模板的局限性

本章节涉及的表达式模板只能解决一趟遍历,新元素值只依赖于当前元素或(和)当前元素之前的元素的问题。矩阵运算这样的新元素依赖多个不同位置元素值的问题不能用表达式模板解决,因为表达式模板本身不存储值,改写今后要使用的元素会让结果不正确。