7

我正在实现一些数学类型,我想优化运算符以最小化创建、销毁和复制的内存量。为了演示,我将向您展示我的四元数实现的一部分。

class Quaternion
{
public:
    double w,x,y,z;

    ...

    Quaternion  operator+(const Quaternion &other) const;
}

我想知道以下两个实现有何不同。我确实有一个 += 实现,它在没有创建内存的地方就地操作,但是一些使用四元数的更高级别的操作使用 + 而不是 += 很有用。

__forceinline Quaternion Quaternion::operator+( const Quaternion &other ) const
{
    return Quaternion(w+other.w,x+other.x,y+other.y,z+other.z);
}

__forceinline Quaternion Quaternion::operator+( const Quaternion &other ) const
{
    Quaternion q(w+other.w,x+other.x,y+other.y,z+other.z);
    return q;
}

我的 c++ 是完全自学的,所以当涉及到一些优化时,我不确定该怎么做,因为我不知道编译器是如何处理这些事情的。此外,这些机制如何转化为非内联实现。

欢迎对我的代码提出任何其他批评。

4

4 回答 4

10

您的第一个示例允许编译器潜在地使用称为“返回值优化”(RVO)的东西。

第二个示例允许编译器潜在地使用称为“命名返回值优化”(NRVO)的东西。这两个优化显然是密切相关的。

可以在此处找到 Microsoft 实施 NRVO 的一些详细信息:

请注意,文章指出 NRVO 支持始于 VS 2005 (MSVC 8.0)。它没有具体说明这是否同样适用于 RVO,但我相信 MSVC 在 8.0 版之前使用了 RVO 优化。

Andrei Alexandrescu 的这篇关于 Move Constructors 的文章提供了有关 RVO 如何工作(以及编译器何时以及为何不使用它)的详细信息。

包括这一点:

听到每个编译器,通常是每个编译器版本,都有自己的检测和应用 RVO 的规则,您会感到失望。有些仅将 RVO 应用于返回未命名临时对象的函数(RVO 的最简单形式)。当函数返回一个命名结果(所谓的命名 RVO,或 NRVO)时,更复杂的也应用 RVO。

本质上,在编写代码时,您可以依靠 RVO 可移植地应用到您的代码中,具体取决于您编写代码的方式(在“精确”的非常流畅的定义下)、月相和鞋子的大小.

这篇文章写于 2003 年,现在编译器应该有很大的改进;希望月相对于编译器可能使用 RVO/NRVO 的时间不那么重要(可能是星期几)。如上所述,似乎 MS 直到 2005 年才实现 NRVO。也许那时在微软从事编译器工作的人得到了一双比以前大半号的更舒适的新鞋。

您的示例很简单,我希望两者都能使用更新的编译器版本生成等效代码。

于 2009-08-21T20:18:59.513 回答
6

在您介绍的两种实现之间,确实没有区别。任何进行任何优化的编译器都会优化您的局部变量。

至于 += 运算符,可能需要稍微更深入地讨论您是否希望您的四元数成为不可变对象......我总是会导致将这样的对象创建为不可变对象。(但话又说回来,我更像是一个托管编码器)

于 2009-08-21T19:16:03.103 回答
2

如果打开优化时这两个实现没有生成完全相同的汇编代码,您应该考虑使用不同的编译器。:) 而且我认为函数是否内联并不重要。

顺便说一句,请注意这__forceinline是非常不便携的。我只会使用普通的旧标准inline,让编译器决定。

于 2009-08-21T19:24:30.870 回答
2

当前的共识是您应该首先实现所有不创建新对象的 ?= 运算符。根据异常安全是否是一个问题(在您的情况下可能不是)或目标, ?= 运算符的定义可能会有所不同。之后你实现运营商?作为使用按值传递语义的 ?= 运算符的自由函数。

// thread safety is not a problem
class Q
{
   double w,x,y,z;
public:
   // constructors, other operators, other methods... omitted
   Q& operator+=( Q const & rhs ) {
      w += rhs.w;
      x += rhs.x;
      y += rhs.y;
      z += rhs.z;
      return *this;
   }
};
Q operator+( Q lhs, Q const & rhs ) {
   lhs += rhs;
   return lhs;
}

这具有以下优点:

  • 只有一种逻辑实现。如果类发生变化,您只需要重新实现 operator?= 和 operator? 会自动适应。
  • 自由函数运算符关于隐式编译器转换是对称的
  • 它是最有效的操作符实现吗?你可以找到关于副本

运营商的效率?

当你打电话给接线员?在两个元素上,必须创建并返回第三个对象。使用上面的方法,复制是在方法调用中执行的。实际上,当您传递临时对象时,编译器能够删除副本。请注意,这应该被理解为“编译器知道它可以删除副本”,而不是“编译器删除副本”。里程会因不同的编译器而异,甚至相同的编译器在不同的编译运行中也会产生不同的结果(由于优化器可用的参数或资源不同)。

在以下代码中,将使用 和 的总和创建一个临时对象,a并且b该临时对象必须再次传递给operator+withc以创建具有最终结果的第二个临时对象:

Q a, b, c;
// initialize values
Q d = a + b + c;

如果operator+具有按值传递语义,编译器可以省略按值传递副本(编译器知道临时对象将在第二次operator+调用后立即被破坏,并且不需要创建不同的副本来传递)

即使operator?可以在代码中将其实现为单行函数 ( Q operator+( Q lhs, Q const & rhs ) { return lhs+=rhs; }),也不应如此。原因是编译器无法知道返回的引用是否operator?=实际上是对同一对象的引用。通过使 return 语句显式获取lhs对象,编译器知道可以省略返回副本。

关于类型的对称性

如果存在从 typeT到 type的隐式转换Q,并且您有两个实例t,并且q分别具有每种类型,那么您期望(t+q)并且(q+t)两者都是可调用的。如果您operator+在内部实现为成员函数Q,则编译器将无法将t对象转换为临时Q对象并稍后调用(Q(t)+q),因为它无法在左侧执行类型转换以调用成员函数。因此,带有成员函数的实现t+q将无法编译。

Note that this is also true for operators that are not symmetric in arithmetic terms, we are talking about types. If you can substract a T from a Q by promoting the T to a Q, then there is no reason not to be able to substract a Q from a T with another automatic promotion.

于 2009-08-21T22:51:36.380 回答