返回值复制问题(以改善调试时序)——这里的解决方案是什么?
我最近遇到的最有趣的 C++ 问题如下:
我们确定(通过分析)我们的算法在 MS Visual Studio 2005 的调试模式下花费了大量时间,并且具有以下类型的函数:
MyClass f(void)
{
MyClass retval;
// some computation to populate retval
return retval;
}
大多数人可能都知道,这里的 return 调用复制构造函数来传递 retval 的副本,然后调用 retval 的析构函数。 (注意:释放模式之所以非常快,是因为返回值优化。但是,我们希望在调试时关闭此功能,以便我们可以介入并在调试器 IDE 中很好地看到内容。)
因此,我们的一个人对此提出了一个很酷的(如果略有缺陷)解决方案,即创建转换运算符:
MyClass::MyClass(MyClass *t)
{
// construct "*this" by transferring the contents of *t to *this
// the code goes something like this
this->m_dataPtr = t->m_dataPtr;
// then clear the pointer in *t so that its destruction still works
// but becomes 'trivial'
t->m_dataPtr = 0;
}
和还将上面的函数更改为:
MyClass f(void)
{
MyClass retval;
// some computation to populate retval
// note the ampersand here which calls the conversion operator just defined
return &retval;
}
现在,在您感到畏缩之前(我在写这篇文章时正在这样做),让我解释一下基本原理。这个想法是创建一个转换运算符,基本上将“内容传输”到新构造的变量。节省的发生是因为我们不再进行深层复制,而是简单地通过指针传输内存。代码调试时间从 10 分钟缩短到 30 秒,正如您可以想象的那样,这对生产力产生了巨大的积极影响。诚然,返回值优化在发布模式下做得更好,但代价是无法介入并观察我们的变量。
当然,大多数人会说“但这是对转换运算符的滥用,你不应该做这种事情”,我完全同意。这是一个为什么你也不应该这样做的例子(这实际上发生了:)
void BigFunction(void)
{
MyClass *SomeInstance = new MyClass;
// populate SomeInstance somehow
g(SomeInstance);
// some code that uses SomeInstance later
...
}
,其中 g
定义为:
void g(MyClass &m)
{
// irrelevant what happens here.
}
现在这是意外发生的,即调用 g() 在需要引用时不应传入指针。但是,没有编译器警告(当然)。编译器确切地知道如何转换,并且它确实这样做了。问题是对
g()
的调用将会(因为我们在它期望 MyClass &
时传递了一个 MyClass *
) ) 调用转换运算符,这是不好的,因为它将 SomeInstance
中的内部指针设置为 0,并且使 SomeInstance
对于调用 之后发生的代码无用>g()
。 ...随之而来的是耗时的调试。
所以,我的问题是,我们如何在调试模式下获得这种加速(具有直接调试时间优势),并使用干净的代码,而不会导致其他可怕错误被忽视?
我还将在这个项目上加点甜头,并在它符合资格后提供我的第一个赏金。 (50 分)
The most interesting C++ question I've encountered recently goes as follows:
We determined (through profiling) that our algorithm spends a lot of time in debug mode in MS Visual Studio 2005 with functions of the following type:
MyClass f(void)
{
MyClass retval;
// some computation to populate retval
return retval;
}
As most of you probably know, the return here calls a copy constructor to pass out a copy of retval
and then the destructor on retval
. (Note: the reason release mode is very fast for this is because of the return value optimization. However, we want to turn this off when we debug so that we can step in and nicely see things in the debugger IDE.)
So, one of our guys came up with a cool (if slightly flawed) solution to this, which is, create a conversion operator:
MyClass::MyClass(MyClass *t)
{
// construct "*this" by transferring the contents of *t to *this
// the code goes something like this
this->m_dataPtr = t->m_dataPtr;
// then clear the pointer in *t so that its destruction still works
// but becomes 'trivial'
t->m_dataPtr = 0;
}
and also changing the function above to:
MyClass f(void)
{
MyClass retval;
// some computation to populate retval
// note the ampersand here which calls the conversion operator just defined
return &retval;
}
Now, before you cringe (which I am doing as I write this), let me explain the rationale. The idea is to create a conversion operator that basically does a "transfer of contents" to the newly constructed variable. The savings happens because we're no longer doing a deep copy, but simply transferring the memory by its pointer. The code goes from a 10 minute debug time to a 30 second debug time, which, as you can imagine, has a huge positive impact on productivity. Granted, the return value optimization does a better job in release mode, but at the cost of not being able to step in and watch our variables.
Of course, most of you will say "but this is abuse of a conversion operator, you shouldn't be doing this kind of stuff" and I completely agree. Here's an example why you shouldn't be doing it too (this actually happened:)
void BigFunction(void)
{
MyClass *SomeInstance = new MyClass;
// populate SomeInstance somehow
g(SomeInstance);
// some code that uses SomeInstance later
...
}
where g
is defined as:
void g(MyClass &m)
{
// irrelevant what happens here.
}
Now this happened accidentally, i.e., the person who called g()
should not have passed in a pointer when a reference was expected. However, there was no compiler warning (of course). The compiler knew exactly how to convert, and it did so. The problem is that the call to g()
will (because we've passed it a MyClass *
when it was expecting a MyClass &
) called the conversion operator, which is bad, because it set the internal pointer in SomeInstance
to 0, and rendered SomeInstance
useless for the code that occured after the call to g()
. ... and time consuming debugging ensued.
So, my question is, how do we gain this speedup in debug mode (which has as direct debugging time benefit) with clean code that doesn't open the possibility to make such other terrible errors slip through the cracks?
I'm also going to sweeten the pot on this one and offer my first bounty on this one once it becomes eligible. (50 pts)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
您需要使用称为“交换优化”的东西。
这将防止复制并在所有模式下保持代码干净。
您也可以尝试与 auto_ptr 相同的技巧,但这有点不确定。
You need to use something called "swaptimization".
This will prevent a copy and keep the code clean in all modes.
You can also try the same trick as
auto_ptr
, but that's more than a little iffy.如果您的
g
定义与代码库中的定义相同,我不确定它是如何编译的,因为不允许编译器将未命名的临时对象绑定到非常量引用。这可能是VS2005的一个bug。如果您使转换构造函数
显式
,那么您可以在函数中使用它(您必须说return MyClass(&retval);
),但它赢了除非明确调用转换,否则不允许在您的示例中调用。或者迁移到 C++11 编译器并使用完整移动语义。
(请注意,实际使用的优化是命名返回值优化或 NRVO)。
If your definition of
g
is written the same as in your code base I'm not sure how it compiled since the compiler isn't allowed to bind unnamed temporaries to non-const references. This may be a bug in VS2005.If you make the converting constructor
explicit
then you can use it in your function(s) (you would have to sayreturn MyClass(&retval);
) but it won't be allowed to be called in your example unless the conversion was explicitly called out.Alternately move to a C++11 compiler and use full move semantics.
(Do note that the actual optimization used is Named Return Value Optimization or NRVO).
出现此问题的原因是您使用
MyClass*
作为魔法设备,有时但并非总是如此。解决办法:使用不同的魔法装置。由于 TempClass 几乎都是私有的(与 MyClass 为友元),因此其他对象无法创建或复制 TempClass。这意味着黑客只能在明确告知的情况下由您的特殊函数创建,以防止意外使用。此外,由于这不使用指针,因此内存不会意外泄漏。
The problem is occuring because you're using
MyClass*
as a magic device, sometimes but not always. Solution: use a different magic device.Since TempClass is almost all private (friending MyClass), other objects cannot create, or copy TempClass. This means the hack can only be created by your special functions when clearly told to, preventing accidental usage. Also, since this doesn't use pointers, memory can't be accidentally leaked.
已经提到了移动语义,您已经同意查找它们以进行教育,所以这很好。这是他们使用的一个技巧。
有一个函数模板
std::move
它将左值转换为右值引用,也就是说它给出了从对象 [*] 移动的“权限”。我相信你可以在你的班级中模仿这个,尽管我不会让它成为一个免费功能:我还没有测试过这个,但是类似的东西。请注意,可以使用实际上是 const 的
MovableMyClass
对象执行一些 const-unsafe 操作,但避免创建其中一个应该比避免创建更容易aMyClass*
(你发现这相当困难!)[*] 实际上我很确定我已经过度简化到了错误的地步,它实际上是关于影响什么选择重载而不是将任何事物“转变”为其他任何事物。但是 std::move 的目的是引起移动而不是复制。
Move semantics have been mentioned, you've agreed to look them up for education, so that's good. Here's a trick they use.
There's a function template
std::move
which turns an lvalue into an rvalue reference, that is to say it gives "permission" to move from an object[*]. I believe you can imitate this for your class, although I won't make it a free function:I haven't tested this, but something along those lines. Note the possibility of doing something const-unsafe with a
MovableMyClass
object that actually isconst
, but it should be easier to avoid ever creating one of those than it is to avoid creating aMyClass*
(which you've found out is quite difficult!)[*] Actually I'm pretty sure I've over-simplified that to the point of being wrong, it's actually about affecting what overload gets chosen rather than "turning" anything into anything else as such. But causing a move instead of a copy is what
std::move
is for.根据您的特殊情况,采用不同的方法:
将
MyClass f(void)
(或operator+
)更改为如下所示:并让
inner_f(c)
code> 保存实际逻辑:然后,为此类测试创建额外的构建配置,其中
TESTING
包含在预处理器定义中。这样,您仍然可以在
f()
中利用 RVO,但实际逻辑不会在您的测试构建上进行优化。请注意,测试构建可以是发布构建,也可以是打开优化的调试构建。无论哪种方式,代码的敏感部分都不会被优化(您可以使用#pragma Optimize
当然,在其他地方也有 - 在上面的代码中它只影响inner_f
本身,而不影响调用的代码从 它)。A different approach, given your special scenario:
Change
MyClass f(void)
(oroperator+
) to something like the following:And let
inner_f(c)
hold the actual logic:Then, create an additional build configurations for this kind of testing, in which
TESTING
is included in the preprocessor definitions.This way, you can still take advantage of RVO in
f()
, but the actual logic will not be optimized on your testing build. Note that the testing build can either be a release build or a debug build with optimizations turned on. Either way, the sensitive parts of the code will not be optimized (you can use the#pragma optimize
in other places too, of course - in the code above it only affectsinner_f
itself, and not code called from it).可能的解决方案
使用一些交换技巧,无论是在复制构造函数中还是像 DeadMG 那样,但我不良心地推荐它们。像这样不合适的复制构造函数可能会导致问题,而后者有点丑陋,需要容易破坏的默认对象,这可能不适用于所有情况。
+1:检查并优化您的复制构造函数,如果它们花费很长时间,则说明它们有问题。
Possible solutions
Use some swap trickery, either in the copy constructor or the way DeadMG did, but I don't recommend them with a good conscience. An inappropriate copy constructor like that could cause problems, and the latter is a bit ugly and needs easily destructible default objects which might not be true for all cases.
+1: Check and optimize your copy constructors, if they take so long then something isn't right about them.
当 MyClass 太大而无法复制时,我更愿意通过引用调用函数来简单地传递对象:
只是简单的 KISS 原则。
I would prefer to simply pass the object by reference to the calling function when
MyClass
is too big to copy:Just simple KISS principle.
好吧,我想我有一个解决方案可以在发布模式下绕过返回值优化,但它取决于编译器并且不能保证有效。 基于此。
至于为什么在DEBUG模式下复制构建需要这么长时间,我不知道。在保持调试模式的同时加速它的唯一可能的方法是使用右值引用和移动语义。您已经通过接受指针的“移动”构造函数发现了移动语义。 C++11 为这种移动语义提供了正确的语法。例子:
Okay I think I have a solution to bypass the Return Value Optimization in release mode, but it depends on the compiler and not guaranteed to work. It is based on this.
As for why the copy construction takes so long in DEBUG mode, I have no idea. The only possible way to speed it up while remaining in DEBUG mode is to use rvalue references and move semantics. You already discovered move semantics with your "move" constructor that accepts pointer. C++11 gives a proper syntax for this kind of move semantics. Example: