咨询手机网站建设平台怎么开网店
表达式模板消除昂贵的中间临时对象
- 引子:中间容器临时的隐形开销
- 表达式模板
- 代码实现
- 使用模板表达式:
- 不使用模板表达式
- 深入剖析:延迟计算(Lazy Evaluation)
- 性能提升归因
- 总结
- 完整代码和运行结果对比
- 一些其他的话
C++ 编程中,我们常常会写出如下表达式:
Vector A, B, C, D; // Vector 底层有一个长度为 n 的数组
A = B + C + D;
引子:中间容器临时的隐形开销
表面上只有一次赋值,背后却是:
auto tmp1 = B + C; // 遍历写入 tmp1.data[0…n-1]
auto tmp2 = tmp1 + D; // 再遍历写入 tmp2.data[0…n-1]
A = tmp2; // 拷贝或移动 entire vector
- 遍历次数:3
- 临时容器:2 个 Vector(tmp1, tmp2)
- 有两次容器级别的内存分配、构造、析构、拷贝/移动
当n很大时,且容器创建时的开销很大时:例如std::vector创建时涉及到堆内存操作,且有一些额外操作。如何减少中间容器是非常重要的
表达式模板
模板表达式是一种延迟计算方法
模板表达式做了两件事:
- 通过引用记录需要操作的对象(减少拷贝)
- 设置需要的计算方式(写好操作符实现)
代码实现
// 表达式模板的基础类
template <typename E>
class Expression {public:double operator[](size_t i) const {return static_cast<const E&>(*this)[i];}size_t size() const { return static_cast<const E&>(*this).size(); }
};
// 向量类的表达式模板版本
class VectorET : public Expression<VectorET> {private:std::vector<double> data;public:VectorET(size_t size) : data(size) {}VectorET(const VectorET& other) : data(other.data) {}VectorET& operator=(const VectorET& other) {data = other.data;return *this;}double& operator[](size_t i) { return data[i]; }const double& operator[](size_t i) const { return data[i]; }size_t size() const { return data.size(); }// 赋值运算符重载 - 支持表达式模板template <typename E>VectorET& operator=(const Expression<E>& expr) {for (size_t i = 0; i < size(); ++i) {data[i] = expr[i];}return *this;}
};
// 加法表达式模板
template <typename E1, typename E2>
class AddExpr : public Expression<AddExpr<E1, E2>> {private:const E1& a;const E2& b;public:AddExpr(const E1& a, const E2& b) : a(a), b(b) {}double operator[](size_t i) const { return a[i] + b[i]; }size_t size() const { return a.size(); }
};
关注AddExpr的实现,并不直接拷贝a和b,而是直接通过引用存储(等同于记录指针),此时AddExpr就是一个表达式,记录了a和b之间是一个加法运算。
计算结果呢?加法运算的结果呢?不好意思,还没有,直到你使用operator[]的时候,才会访问a、b的第i个元素,并进行计算返回结果。
这就是延迟计算,结果并不在a+b的时候就已经算出来了,而是在(a+b)[i]的时候才进行计算,并且没有容器级的拷贝,就算有拷贝,也只针对元素进行。
所以最终的结果可以通过一次遍历执行。
我们对比使用和没有使用模板表达式的情况
A = B + C + D;
使用模板表达式:
- B + C生成一个中间变量AddExpr<VectorET, VectorET>,但是没有容器拷贝,
- AddExpr<VectorET, VectorET> + D 生成第二个中间变量AddExpr<AddExpr<VectorET, VectorET>, VectorET>,没有容器拷贝
- AddExpr<AddExpr<VectorET, VectorET>, VectorET>赋值给A,走VectorET的赋值逻辑,对expr[i]进行遍历,expr[i]调用AddExpr::operator[]操作,会递归调用每一个元素的operator[]运算。
template <typename E>VectorET& operator=(const Expression<E>& expr) {for (size_t i = 0; i < size(); ++i) {data[i] = expr[i];}return *this;}
对于没有使用模板表达式的操作,这种行为等同于,减少了中间变量的生成
for (size_t i = 0; i < n; ++i) {A[i] = (B + C + D)[i];
}
不使用模板表达式
A = B + C + D
,还会额外生成两个容器中间变量,这两个容器的中间变量如果是std::vector,开销会非常可观。
深入剖析:延迟计算(Lazy Evaluation)
表达式模板的核心在于**“惰性计算”**:
- 每次 operator+ 不计算值,只生成一个描述如何计算的节点对象。
- 这些节点彼此组合,形成一棵表达式树,但不存储任何中间结果。
- 当执行 A = expr; 时,沿树一次性完成所有元素级计算:
- 无中间容器:避免堆分配、构造、析构
- 寄存器级临时:标量相加只在寄存器中短暂存在,不触发对象构造
- 高缓存命中:一次访问避免多次循环
for (size_t i = 0; i < n; ++i) {A[i] = B[i] + C[i] + D[i];
}
之前博主在做分析时,还很纳闷,为什么ai一致说会减少中间变量的生成,后来通过汇编发现了ai漏掉的两个点,或者说ai可能认为这个点不需要重点描述确实非常重要的:
- 容器的生成和元素的生成开销是不一样的,假设你有一个n个对象的容器,对比n个对象的生成,加上了容器进行包装,就意味着多了一个容器生成的开销,特别是对于std::vector来说更是如此。假设我们有一个
std::vector<double> val(1000);
,和doube val[1000]
,前者的开销大很多,因为std::vector的空间是在堆上申请的,但是double val[1000]的空间是在栈上申请的,申请空间的开销甚至不是一个数量级的!- 编译器对于标量级别的临时变量会做优化,很可能就直接把中间结果放到了寄存器中,而不用构造中间变量了。这就意味着
a = b + c + d
会生成两个Vector的中间变量,而a[i] = b[i] + c[i] + d[i],如果a[i]是一个int/double这些,很可能都不会生成中间变量。
性能提升归因
- 合并遍历:3 次→1 次
- 消除堆分配/析构:临时容器不再出现
- 提升数据局部性:连续内存访问
总结
通过表达式模板的惰性计算策略,你可以在大型矩阵、图像处理、物理仿真等高性能场景中,零临时容器、单次遍历地完成复杂链式运算,显著提升性能。
完整代码和运行结果对比
#include <chrono>
#include <iostream>
#include <random>
#include <vector>// 传统向量类
class Vector {private:std::vector<double> data;public:Vector(size_t size) : data(size) {}Vector(const Vector& other) : data(other.data) {}Vector& operator=(const Vector& other) {data = other.data;return *this;}double& operator[](size_t i) { return data[i]; }const double& operator[](size_t i) const { return data[i]; }size_t size() const { return data.size(); }// 传统向量加法Vector operator+(const Vector& other) const {Vector result(size());for (size_t i = 0; i < size(); ++i) {result[i] = data[i] + other[i];}return result;}// 传统向量减法Vector operator-(const Vector& other) const {Vector result(size());for (size_t i = 0; i < size(); ++i) {result[i] = data[i] - other[i];}return result;}// 传统向量乘法(与标量)Vector operator*(double scalar) const {Vector result(size());for (size_t i = 0; i < size(); ++i) {result[i] = data[i] * scalar;}return result;}
};// 表达式模板的实现// 表达式模板的基础类
template <typename E>
class Expression {public:double operator[](size_t i) const {return static_cast<const E&>(*this)[i];}size_t size() const { return static_cast<const E&>(*this).size(); }
};// 向量类的表达式模板版本
class VectorET : public Expression<VectorET> {private:std::vector<double> data;public:VectorET(size_t size) : data(size) {}VectorET(const VectorET& other) : data(other.data) {}VectorET& operator=(const VectorET& other) {data = other.data;return *this;}double& operator[](size_t i) { return data[i]; }const double& operator[](size_t i) const { return data[i]; }size_t size() const { return data.size(); }// 赋值运算符重载 - 支持表达式模板template <typename E>VectorET& operator=(const Expression<E>& expr) {for (size_t i = 0; i < size(); ++i) {data[i] = expr[i];}return *this;}
};// 加法表达式模板
template <typename E1, typename E2>
class AddExpr : public Expression<AddExpr<E1, E2>> {private:const E1& a;const E2& b;public:AddExpr(const E1& a, const E2& b) : a(a), b(b) {}double operator[](size_t i) const { return a[i] + b[i]; }size_t size() const { return a.size(); }
};// 减法表达式模板
template <typename E1, typename E2>
class SubExpr : public Expression<SubExpr<E1, E2>> {private:const E1& a;const E2& b;public:SubExpr(const E1& a, const E2& b) : a(a), b(b) {}double operator[](size_t i) const { return a[i] - b[i]; }size_t size() const { return a.size(); }
};// 乘法表达式模板(与标量)
template <typename E>
class MulExpr : public Expression<MulExpr<E>> {private:const E& a;double scalar;public:MulExpr(const E& a, double scalar) : a(a), scalar(scalar) {}double operator[](size_t i) const { return a[i] * scalar; }size_t size() const { return a.size(); }
};// 运算符重载,返回表达式对象而不是实际计算结果
template <typename E1, typename E2>
AddExpr<E1, E2> operator+(const Expression<E1>& a, const Expression<E2>& b) {return AddExpr<E1, E2>(static_cast<const E1&>(a),static_cast<const E2&>(b));
}template <typename E1, typename E2>
SubExpr<E1, E2> operator-(const Expression<E1>& a, const Expression<E2>& b) {return SubExpr<E1, E2>(static_cast<const E1&>(a),static_cast<const E2&>(b));
}template <typename E>
MulExpr<E> operator*(const Expression<E>& a, double scalar) {return MulExpr<E>(static_cast<const E&>(a), scalar);
}template <typename E>
MulExpr<E> operator*(double scalar, const Expression<E>& a) {return MulExpr<E>(static_cast<const E&>(a), scalar);
}// 性能测试函数
void runTraditionalTest(size_t size, int iterations) {// 初始化向量Vector a(size), b(size), c(size), d(size), result(size);std::random_device rd;std::mt19937 gen(rd());std::uniform_real_distribution<> dis(0.0, 1.0);for (size_t i = 0; i < size; ++i) {a[i] = dis(gen);b[i] = dis(gen);c[i] = dis(gen);d[i] = dis(gen);}auto start = std::chrono::high_resolution_clock::now();// 执行多次计算以测量性能for (int iter = 0; iter < iterations; ++iter) {result = a + b - c * 2.0 + d * 3.0;}auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> elapsed = end - start;std::cout << "传统方法: " << elapsed.count() << " ms" << std::endl;
}void runExpressionTemplateTest(size_t size, int iterations) {// 初始化向量VectorET a(size), b(size), c(size), d(size), result(size);std::random_device rd;std::mt19937 gen(rd());std::uniform_real_distribution<> dis(0.0, 1.0);for (size_t i = 0; i < size; ++i) {a[i] = dis(gen);b[i] = dis(gen);c[i] = dis(gen);d[i] = dis(gen);}auto start = std::chrono::high_resolution_clock::now();// 执行多次计算以测量性能for (int iter = 0; iter < iterations; ++iter) {result = a + b - c * 2.0 + d * 3.0;}auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> elapsed = end - start;std::cout << "表达式模板: " << elapsed.count() << " ms" << std::endl;
}int main() {const size_t size = 1000000;const int iterations = 100;std::cout << "向量大小: " << size << ", 迭代次数: " << iterations<< std::endl;// 先运行传统方法,再运行表达式模板方法runTraditionalTest(size, iterations);runExpressionTemplateTest(size, iterations);return 0;
}
运行结果
一些其他的话
- 如果容器复制的开销很小,模板表达式的优点也就没那么明显了,例如把std::vector换成std::array,甚至有可能生成模板的开销更大[狗头]。