表达式模板(Expression Templates)
  ZXL8ALkrtBPG 2023年11月02日 83 0
cpp

C++表达式为立即求值,例如x + y - z表达式,x + y会产生一个临时变量进行存储;对应向量表达式同时具有可读性(重载operator),在C++中我们容易得到以下实现:

template<typename T>
class Array {
public:
    T &operator[](size_t index)
    {   
        return m_arr[index];
    }   

    T operator[](size_t index) const
    {
        return m_arr[index];
    }   

    size_t Size() const
    {   
        return m_arr.size();
    }   

    Array(size_t size, const T& val) : m_arr(size, val)
    {   
        // DBG_LOG("");
    }   

private:
    std::vector<T> m_arr;
};

template<typename T>
Array<T> operator+(const Array<T>& a1, const Array<T>& a2) 
{
    Array<T> result(a1.Size(), 0); 
    for (int i = 0; i < a1.Size(); i++) {
        result[i] = a1[i] + a2[i];
    }   
    return result;
}

template<typename T>
Array<T> operator-(const Array<T>& a1, const Array<T>& a2) 
{
    Array<T> result(a1.Size(), 0); 
    for (int i = 0; i < a1.Size(); i++) {
        result[i] = a1[i] + a2[i];
    }
    return result;
}
Array<int> a1(SIZE, 2); 
        Array<int> a2(SIZE, 2); 
        Array<int> a3(SIZE, 1); 
        (a1 + a2 - a3); // 构造了临时向量, 带来了性能和内存上的开销

立即求值的方式存在额外开销,对应向量规模大的情况下,临时变量的产生对性能和内存都是不小的代价;

在函数式编程中表达式的计算通常是延迟的,这样可以按需组合表达式同时不会带来性能和内存上的开销【1】。C++中表达式模板利用模板元编程技术达到计算延迟的效果;

表 达 式 模 板 是 由 Todd Veldhuizen在 1995年 6月 在 一 篇 文 章 中 给 出 的 。

表 达 式 模 板 是 一 种 C++模 板 元 编 程 (template metaprogramming)技 术 。 典 型 情 况 下 , 表 达 式 模 板 自 身 代 表 一 种 操 作 , 模 板 参 数 代 表 该 操 作 的 操 作 数 。

模 板 表 达 式 可 将 子 表 达 式 的 计 算 推 迟 , 这 样 有 利 于 优 化 (特 别 是 减 少 临 时 变 量 的 使 用 )。 表 达 式 模 板 也 可 以 作 为 参 数 传 递 给 一 个 函 数 。

向量延迟计算

先看代码再展开分析实现:

namespace ExpTemplates {
template<typename Derive>
class ExpBase {
public:
    operator const Derive&() const // (const Derive&) (Xxx), 类型转换
    {   
        return static_cast<const Derive&>(*this);
    }   
};

template<typename T>
class Array : public ExpBase<Array<T>> {
public:
    T &operator[](size_t index)
    {   
        return m_arr[index];
    }   

    T operator[](size_t index) const
    {   
        return m_arr[index];
    }   

    size_t Size() const
    {   
        return m_arr.size();
    }   
    
    Array(size_t size, const T& val) : m_arr(size, val)
    {   
        // DBG_LOG("");
    }   

    template<typename Derive>
    Array(const ExpBase<Derive>& e)
    {   
        const Derive& d = e;
        m_arr.resize(d.Size());
        for (int i = 0; i < d.size(); i++) {
            m_arr[i] = d[i];
        }
    }   

private:
    std::vector<T> m_arr;
};

template<typename T1, typename T2>
class ArrayAdd : public ExpBase<ArrayAdd<T1, T2>> {
public:
    ArrayAdd(const ExpBase<T1>& e1, const ExpBase<T2>& e2): m_arr1(e1), m_arr2(e2)
    {
    }
    auto operator[](size_t index) const
    {
        return (m_arr1[index] + m_arr2[index]);
    }
private:
    const T1& m_arr1;
    const T2& m_arr2;
};

template<typename T1, typename T2>
class ArrayDiff : public ExpBase<ArrayDiff<T1, T2>> {
public:
    ArrayDiff(const ExpBase<T1>& e1, const ExpBase<T2>& e2): m_arr1(e1), m_arr2(e2)
    {
    }
    auto operator[] (size_t index) const
    {
        return (m_arr1[index] - m_arr2[index]);
    }
private:
    const T1& m_arr1;
    const T2& m_arr2;
};

template<typename T1, typename T2>
auto operator+(const ExpBase<T1>& e1, const ExpBase<T2>& e2)
{
    return ArrayAdd(e1, e2);
}

template<typename T1, typename T2>
auto operator-(const ExpBase<T1>& e1, const ExpBase<T2>& e2)
{
    return ArrayDiff(e1, e2);
}
}

此时(a1 + a2 - a3)表达式的类型为

ExpTemplates::ArrayDiff<ExpTemplates::ArrayAdd<ExpTemplates::Array<int>, ExpTemplates::Array<int> >, ExpTemplates::Array<int> >

在编译期记录了哪些操作,每个操作对应的对象参数,例如上述代码中将加法作为一个操作对象记录在最终的表达式类型中,最终的表达式对象并没有立即对参数进行求和或者相减,而是在调用operatore[]时才真正开始计算,性能上通过表达式模板也会有比较明显的提升;

|               ns/op |                op/s |    err% |     total | benchmark
|--------------------:|--------------------:|--------:|----------:|:----------
|        3,310,100.00 |              302.11 |    4.8% |      0.04 | `operator overload impl`
|          619,400.00 |            1,614.47 |    2.2% |      0.01 | `expression templates impl`

运行态分析

这里简单补充下运行时的分析,便于读者快速理解上述代码:

表达式模板(Expression Templates)_cpp

上述代码中使用的容器为vector,可以改进为更加泛化的实现;

template<typename T1, typename T2, typename Operation>
struct ExpBase {
    ExpBase(const T1& left, const T2& right, const Operation& op)
        : m_left(left), m_right(right), m_op(op)
    {};

protected:
    const T1 &m_left;
    const T2 &m_right;
    Operation m_op;
};

template<typename T1, typename T2, typename Operation>
struct ExpTemplates : private ExpBase<T1, T2, Operation> {
    using Base = ExpBase<T1, T2, Operation>;
    using Base::Base;
    auto operator[](size_t index)
    {
        return Base::m_op(Base::m_left[index], Base::m_right[index]);
    }
};

template<typename T1, typename T2>
auto operator+(const T1& left, const T2& right)
{
    auto add = [](auto l, auto r){ return l + r; };
    return ExpTemplates<T1, T2, decltype(add)>(left, right, add);
}

参考资料

【1】C++20高级编程

【2】C++ Templates

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  ZXL8ALkrtBPG   2023年11月02日   50   0   0 cpp
  PQYWJLBjS0G7   2023年11月02日   49   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   69   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   40   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   39   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   48   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   83   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   35   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   29   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   36   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   18   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   59   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   70   0   0 Listcpp
  ZXL8ALkrtBPG   2023年11月02日   47   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   69   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   31   0   0 cpp
ZXL8ALkrtBPG