按值返回内联函数

时间:2009-08-21 19:08:35

标签: c++ optimization memory inline

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

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 个答案:

答案 0 :(得分:10)

您的第一个示例允许编译器可能使用称为“返回值优化”(RVO)的某些内容。

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

微软实施NRVO的一些细节可以在这里找到:

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

This article about Move Constructors by Andrei Alexandrescu提供了有关RVO如何工作的详细信息(以及编译器何时以及为何不使用它)。

包括这一位:

  

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

     

从本质上讲,在编写代码时,您可以依赖于您可以根据您编写代码的方式(在“精确”的非常流畅的定义下),月亮的相位和大小,将RVO可移植地应用于您的代码你的鞋子。

该文章写于2003年,编译器现在应该大大改进;希望月亮的阶段对于编译器可能使用RVO / NRVO时可能不那么重要(也许它可以用到星期几)。如上所述,似乎MS直到2005年才开始实施NRVO。也许就是那时,在微软的编译器上工作的人会得到一双比以前大一半的新鞋。

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

答案 1 :(得分:6)

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

对于+ =运算符,可能需要稍微讨论一下你是否希望你的Quaternions是不可变对象......我总是会把这样的对象创建为不可变对象。 (但话又说回来,我更像是一个管理编码器)

答案 2 :(得分:2)

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

顺便提一下,请注意__forceinline非常不便携。我只想使用普通的旧标准inline并让编译器决定。

答案 3 :(得分:2)

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

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

这具有以下优点:

  • 只有一个逻辑实现。如果类更改,您只需要重新实现运算符?=和运算符?将自动适应。
  • 自由函数运算符在隐式编译器转换方面是对称的
  • 这是运营商最有效的实施方式吗?你可以找到关于副本的文件

运营商的效率?

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

在以下代码中,将创建一个临时值ab的临时值,并且该临时值必须与operator+一起再次传递给c用最终结果创建第二个临时文件:

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对象,编译器知道可以省略返回副本。

关于类型的对称性

如果存在从类型T到类型Q的隐式转换,并且每个类型分别有两个tq个实例,那么您期望{{ 1}}和(t+q)都可以调用。如果在(q+t)中将operator+实现为成员函数,则编译器将无法将Q对象转换为临时t对象,并稍后调用{{1因为它无法在左侧执行类型转换来调用成员函数。因此,使用成员函数实现Q将无法编译。

请注意,对于算术术语不对称的运算符也是如此,我们讨论的是类型。如果您可以通过将(Q(t)+q)推广到t+q来从T中减去Q,那么就没有理由不能减去TQ进行另一次自动宣传。