为什么现代编译器没有消除表达式模板的需求?

3
标准做法是在C++中使用表达式模板来提高效率,通过消除不必要的临时对象。为什么C++编译器不能自动删除这些不必要的临时对象呢?
这个问题我认为已经知道答案,但由于无法在网上找到低级别的回答,因此希望确认一下。
表达式模板基本上允许/强制进行极端程度的内联。然而,即使进行内联,编译器也无法通过优化调用operator new和operator delete来删除这些多余的临时对象,因为它们将这些调用视为不透明的,因为这些调用可以在其他翻译单元中被重写。表达式模板完全删除了这些中间对象的调用。
可以在一个简单的例子中看到这些多余的operator new和operator 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;
}

生成的代码中,我们可以看到foo()编译成了一个相对较长的函数,其中调用了两个operator new和一个operator delete,而bar()只编译成了寄存器的传递,并且没有进行任何不必要的拷贝。
这个分析正确吗?
在C++中,是否有可能合法地省略foo()中的复制操作?

1
[expr.new#10]及以下内容... - Massimiliano Janes
3
关于表达式模板,我认为它们的目标并不是仅仅为了内联,而是更加利用表达式所表示的特定领域语言类型系统中的对称性。例如,当你要计算三个共轭矩阵相乘时,表达式模板可以使用一个专门针对这种情况进行空间时间优化的算法;编译器可不知道这些代数运算... :) - Massimiliano Janes
2
我认为你不知道什么是“表达式模板”。在你展示的代码中没有任何相关内容。 - bolov
1
@Praxeolitic 我仍然看不出你的例子中有表达式模板起到的作用。是的,表达式模板的目标是消除临时对象,但更重要的是消除中间操作。 但是,这仅限于一个完整表达式内部。它们与您的示例没有任何关系。[cont] - bolov
1
一个展示表达式模板的简单代码是这样的:Matrix a, b, c, d = ....; Matrix r = a + b * c + d; 表达式模板在表达式 a + b * c + d 中发挥作用,这种技术所做的是,它不会立即计算 b*c,然后计算 a + temp1,再计算 temp2 + d,而是将算术计算延迟到赋值时。它通过使用模板创建一个在编译时已知的 AST 对象来实现这一点,用于表达式 a + b * c + d - bolov
显示剩余5条评论
2个回答

4
然而,即使进行内联,编译器也无法优化掉对operator new和operator delete的调用,因为这些调用被视为不透明,因为这些调用可以在其他翻译单元中被覆盖。自从c++14以来,这种情况不再成立,只要满足一定条件,分配调用就可以被优化/重用。一个实现允许省略对可替换全局分配函数的调用。当这样做时,存储将由实现提供或由扩展另一个new表达式的分配提供。因此,现在foo()可以合法地被优化为等效于bar()...
Expression templates本质上允许/强制极端内联。在我看来,表达式模板的重点不在于内联本身,而是利用表达式所模拟的领域特定语言类型系统的对称性。例如,当你乘以三个共轭矩阵时,表达式模板可以使用一个空间时间优化算法,利用乘积是结合的事实和共轭矩阵是伴随对称的事实,从而减少总操作次数(甚至可能获得更好的精度)。而且,所有这些都发生在编译时。相反,编译器无法知道什么是共轭矩阵,它被限制通过你的实现浮点语义来评估表达式。

3
有两种表达式模板。
一种是关于直接嵌入C ++的领域特定语言。 Boost.Spirit将表达式转换为递归下降解析器。Boost.Xpressive将它们转换为正则表达式。老旧的Boost.Lambda将它们转换为带有参数占位符的函数对象。
显然,编译器无法消除这种需求。要添加eDSL添加的功能,就需要特殊用途的语言扩展,就像lambda被添加到C ++ 11中一样。但是,为每个编写的eDSL这样做并不是生产性的;它会使语言庞大且难以理解,以及其他问题。
第二类是保持高级语义相同但优化执行的表达式模板。他们将领域特定知识应用于将表达式转换为更有效的执行路径,同时保持语义不变。线性代数库可能会在Massimiliano解释的答案中这样做,或者像Boost.Simd这样的SIMD库可能将多个操作转换为单个融合操作,例如乘法-加法。
这些库提供了理论上编译器可以执行而不修改语言规范的服务。但是,为了这样做,编译器必须认识相关领域,并拥有所有内置领域知识来进行转换。这种方法过于复杂,会使编译器变得庞大,甚至比它们本来就慢。
对于这些库的表达式模板的另一种替代方法将是编译器插件,即不是编写具有所有表达式模板魔法的特殊矩阵类,而是为编译器编写插件,该插件了解矩阵类型并转换编译器使用的AST。这种方法的问题在于,编译器必须同意插件API(不会发生,它们在内部工作方式上有太大的差异),或者库作者必须为他们希望其库可用(或至少性能良好)的每个编译器编写单独的插件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接