3

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;
}

生成的代码中,我们看到foo()编译为一个相对冗长的函数,其中两次调用operator new和一次调用operator deletewhilebar()编译为仅传输寄存器并且不进行任何不必要的复制。

这个分析正确吗?

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

4

2 回答 2

4

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

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

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

所以 foo() 现在可以合法地优化为等效于 bar() 的东西......


表达式模板本质上允许/强制进行极端程度的内联

IMO 表达式模板的重点与内联本身无关,而是利用表达式模型的领域特定语言的类型系统的对称性。

例如,当您将三个 Hermitian 矩阵相乘时,表达式模板可以使用时空优化算法,利用乘积是关联的并且 Hermitian 矩阵是伴随对称的这一事实,从而减少总操作数 (甚至可能更好的准确性)。而这一切,都发生在编译时。

相反,编译器无法知道厄米特矩阵是什么,它限制了以暴力方式评估表达式(根据您的实现浮点语义)。

于 2017-12-07T08:39:44.883 回答
3

有两种表达式模板。

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

显然,编译器无法摆脱这种需求。需要特殊用途的语言扩展来添加 eDSL 添加的功能,例如将 lambdas 添加到 C++11。但是对每一个编写的 eDSL 都这样做是没有效率的。除其他问题外,这将使语言变得庞大且无法理解。

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

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

这种库的表达式模板的另一种方法是编译器插件,也就是说,您无需编写具有所有表达式模板魔术的特殊矩阵类,而是为编译器编写一个插件,该插件了解矩阵类型并转换 AST编译器使用。这种方法的问题在于,要么编译器必须就插件 API 达成一致(不会发生,它们在内部的工作方式差异太大),要么库作者必须为他们希望其库可用的每个编译器编写一个单独的插件(或至少性能)。

于 2017-12-07T09:11:20.340 回答