我正在实现一些数学类型,我想优化运算符以最小化创建,销毁和复制的内存量。为了演示,我将向您展示我的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 ++完全是自学成才,所以当谈到一些优化时,我不确定该怎么做,因为我不确切知道编译器如何处理这些事情。这些机制如何转换为非内联实现。
欢迎对我的代码提出任何其他批评。
答案 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;
}
这具有以下优点:
运营商的效率?
当你打电话给接线员?在两个元素上,必须创建并返回第三个对象。使用上述方法,复制在方法调用中执行。实际上,当您传递临时对象时,编译器能够忽略该副本。请注意,这应该被理解为'编译器知道它可以忽略副本',而不是'编译器将忽略副本'。里程因不同的编译器而异,即使是相同的编译器也可能在不同的编译运行中产生不同的结果(由于优化器可用的参数或资源不同)。
在以下代码中,将创建一个临时值a
和b
的临时值,并且该临时值必须与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
的隐式转换,并且每个类型分别有两个t
和q
个实例,那么您期望{{ 1}}和(t+q)
都可以调用。如果在(q+t)
中将operator+
实现为成员函数,则编译器将无法将Q
对象转换为临时t
对象,并稍后调用{{1因为它无法在左侧执行类型转换来调用成员函数。因此,使用成员函数实现Q
将无法编译。
请注意,对于算术术语不对称的运算符也是如此,我们讨论的是类型。如果您可以通过将(Q(t)+q)
推广到t+q
来从T
中减去Q
,那么就没有理由不能减去T
从Q
进行另一次自动宣传。