为什么现代编译器不再需要表达式模板?

时间:2017-12-07 07:56:24

标签: c++ templates expression-templates

C ++中表达式模板的标准间距是它们通过删除不必要的临时对象来提高效率。为什么C ++编译器不能删除这些不必要的临时对象?

这个问题我认为我已经知道了答案,但我想确认,因为我无法在网上找到一个低级答案。

表达式模板基本上允许/强制进行极端内联。但是,即使使用内联,编译器也无法优化对operator newoperator delete的调用,因为他们将这些调用视为不透明,因为这些调用可以在其他翻译单元中被覆盖。表达式模板完全删除了对中间对象的调用。

这些对operator newoperator delete的多余调用可以在一个我们只复制的简单示例中看到:

#include <array>
#include <vector>

std::vector<int> foo(std::vector<int> x)
{
    std::vector<int> y{x};
    std::vector<int> z{y};
    return z;
}

std::array<int, 3> bar(std::array<int, 3> x)
{
    std::array<int, 3> y{x};
    std::array<int, 3> z{y};
    return z;
}

generated code中,我们看到foo()编译为一个相对冗长的函数,其中两次调用operator new,一次调用operator deletebar()编译仅转移寄存器而不进行任何不必要的复制。

这种分析是否正确?

任何C ++编译器都可以合法地删除foo()中的副本吗?

2 个答案:

答案 0 :(得分:4)

  

然而,即使使用内联,编译器也无法优化对operator new和operator delete的调用,因为它们将这些调用视为不透明,因为这些调用可以在其他翻译单元中被覆盖。

从c ++ 14开始,这不再是真的,在某些条件下可以优化/重用分配调用:

  

[expr.new#10] 允许实现省略对可替换全局分配函数的调用。当它这样做时,存储由实现提供,或者通过扩展另一个新表达式的分配来提供。[条件如下]

所以foo()可以合法地优化到等于bar()的东西......

  

表达式模板基本上允许/强制进行极端内联

IMO表达模板的重点并不是内联本身,而是利用域特定语言表达模型的类型系统的对称性。 / p>

例如,当你将三个,例如,Hermitian矩阵相乘时,表达式模板可以使用时空优化算法,利用产品 associative 的事实,并且Hermitian矩阵是伴随对称的,导致总操作次数减少(甚至可能更精确)。所有这些都发生在编译时。

相反,编译器无法知道Hermitian矩阵是什么,它受限于以粗暴的方式评估表达式(根据您的实现浮点语义)。

答案 1 :(得分:3)

有两种表达模板。

一种是关于直接嵌入C ++的领域特定语言。 Boost.Spirit将表达式转换为递归下降解析器。 Boost.Xpressive将它们变成正则表达式。好旧的Boost.Lambda将它们变成带有参数占位符的函数对象。

显然,编译器无法摆脱这种需求。需要使用特殊用途的语言扩展来添加eDSL添加的功能,例如将lambdas添加到C ++ 11中。但是,对于每个编写的eDSL而言,这样做并不高效;除了其他问题之外,它会使语言变得巨大而且无法理解。

第二种是表达式模板,它们保持高级语义相同但优化执行。它们应用特定于域的知识将表达式转换为更高效的执行路径,同时保持语义相同。线性代数库可能会像Massimiliano在他的回答中解释的那样,或像Boost.Simd这样的SIMD库可能会将多个操作转换为单个融合操作,如multiply-add。

这些库提供的服务在理论上可以在不修改语言规范的情况下执行。但是,为了做到这一点,编译器必须识别有问题的域并具有所有内置的域知识来进行转换。这种方法过于复杂,会使编译器变得庞大甚至比它们更慢。

这些类库的表达模板的另一种方法是编译器插件,即不是编写具有所有表达式模板魔法的特殊矩阵类,而是为编译器编写一个知道矩阵类型和变换的插件编译器使用的AST。这种方法的问题在于,要么编译器必须就插件API达成一致(不会发生,内部工作方式太不相同),或者库作者必须为每个希望其库可用的编译器编写单独的插件(或者至少表现出来。