从临时对象创建复合对象可以被优化吗?

4

我已经问了一些与这个问题有关的问题,但是得到的回答不一,所以我认为最好直接问。

假设我们有以下代码:

// Silly examples of A and B, don't take so seriously, 
// just keep in mind they're big and not dynamically allocated.
struct A { int x[1000]; A() { for (int i = 0; i != 1000; ++i) { x[i] = i * 2; } };
struct B { int y[1000]; B() { for (int i = 0; i != 1000; ++i) { y[i] = i * 3; } };

struct C
{
  A a;
  B b;
};

A create_a() { return A(); }
B create_b() { return B(); }

C create_c(A&& a, B&& b)
{
  C c;
  c.a = std::move(a);
  c.b = std::move(b);
  return C; 
};

int main()
{
  C x = create_c(create_a(), create_b());
}

现在理想情况下,create_c(A&&, B&&) 应该是一个空操作。A和B不应该在栈上创建并传递它们的引用,而是应该直接按值创建并传递到返回值c的位置。使用NRVO,这将意味着直接创建并将它们传递到x中,函数create_c不需要再做任何额外的工作。

这将避免创建A和B的副本。

是否有任何方法可以让编译器允许/鼓励/强制执行此行为?或者优化编译器通常会自动执行此操作吗?这只有在编译器内联函数时才起作用,还是跨编译单元也能起作用?

(我认为这可能会跨编译单元工作...)

如果create_a()create_b()带有一个隐藏参数来放置返回值,则它们可以直接将结果放入x中,然后通过引用传递给create_c(),后者无需进行任何操作即可立即返回。

3个回答

4

您有不同的方式来优化代码,但是右值引用并不是其中之一。问题在于既无法免费移动(move)A,也无法免费窃取(steal)对象的内容。请考虑以下示例:

template <typename T>
class simple_vector {
   typedef T element_type;
   typedef element_type* pointer_type;
   pointer_type first, last, end_storage;
public:
   simple_vector() : first(), last(), end_storage() {}
   simple_vector( simple_vector const & rhs )              // not production ready, memory can leak from here!
      : first( new element_type[ rhs.last - rhs.first ] ),
        last( first + rhs.last-rhs.first ),
        end_storage( last )
   {
       std::copy( rhs.first, rhs.last, first );
   }
   simple_vector( simple_vector && rhs ) // we can move!
      : first( rhs.first ), last( rhs.last ), end_storage( rhs.end_storage )
   {
      rhs.first = rhs.last = rhs.end_storage = 0;
   }
   ~simple_vector() {
      delete [] rhs.first;
   }
   // rest of operations
};

在这个例子中,由于资源是通过指针持有的,因此有一种简单的方法来“移动”对象(即将旧对象的内容窃取到新对象中并使旧对象处于可破坏但无用的状态。只需复制指针并在旧对象中将它们重置为null,以便原始对象析构函数不会释放内存。
A和B的问题都在于实际内存是通过数组在对象内部保存的,而该数组不能被移动到新的C对象的不同内存位置。
当然,由于你在代码中使用堆栈分配的对象,编译器可以使用旧的(N)RVO,当你执行C c = {create_a(),create_b()};时,编译器可以执行该优化(基本上将属性c.a设置为从create_a返回的对象地址,同时在编译create_a时,直接在同一地址上创建返回的临时对象,因此,c.a,从create_a返回的对象和在create_a内部构造的临时对象(隐式this到构造函数)相同的对象,避免了两次拷贝。对于c.b也可以做同样的事情,避免拷贝成本。如果编译器内联你的代码,它将删除create_c并用类似于以下构造的构造替换它:C c = {create_a(),create_b()};,因此它可能会优化掉所有的拷贝。
值得注意的是,如果像C* p = new C; p->a = create_a();这样动态分配一个C对象,这种优化不能完全使用,因为目标不在堆栈中,编译器只能优化create_a中的临时变量和其返回值,但不能使其与p->a重合,因此需要进行拷贝。这是rvalue-references相对于(N)RVO的优点,但如前所述,你不能直接在你的代码示例中有效地使用rvalue-references。

3

在您的情况下,有两种优化可以应用:

  1. 函数内联(对于A、B和C(以及它们包含的A和B)的情况)
  2. 复制省略(仅对C(以及它包含的A和B),因为您通过值返回了C)

对于这么小的一个函数,很可能会被内联。如果它存在于同一翻译单元中,大多数编译器都会这么做。像MSVC++和G++这样的好编译器还有整个程序优化设置,可以跨翻译单元进行内联。如果该函数被内联,则调用该函数(以及随之而来的复制)将不会发生。

如果由于某些原因函数没有被内联(例如在MSVC++中使用__declspec(noinline)),那么您仍然有资格获得命名返回值优化(NRO),好的C++编译器(再次是MSVC++、G++和我认为的LLVM)都实现了这一点。基本上,标准规定编译器可以避免执行返回时的复制操作,它们通常会生成避免复制的代码。有一些事情可以做来禁用NRVO,但大多数情况下,这是一个相当安全的优化依赖。
最后,进行性能分析。如果发现性能问题,则需要找出其他解决方案。否则,应以惯用方式编写代码,并仅在需要时用更高效的构造替换它们。

0

很明显的做法不是给C一个构造函数然后说:

C create_c(const A & a, const B & b)
{
  return C( a, b );
}

这个程序有很多优化的可能性。或者干脆不使用 create 函数。我认为这不是一个非常好的激励示例。


@unapersson:这怎么消除A和B的副本?你能详细说明一下吗? - Clinton
@Clinton 你不能消除所有 A 和 B 的拷贝,因为 C 最终需要包含它们的拷贝。 - user2100815
@ildjam:我不完全同意,只需删除create_c并执行C c = { create_a(), create_b() };,您现在就可以基本上获得相同的结果,而无需等待以后重构类型AB。如果编译器进行了内联,那甚至可能已经是这种情况了。语义已经足够清晰,不需要rvalue-references,而且一个好的老编译器(实现当前标准)现在已经可以通过内联优化所有副本(通过内联)。 - David Rodríguez - dribeas
我想说的不是你不应该使用移动语义,而是如果对象不可移动,则没有意义。移动语义最初并非为日常使用而设计,而是为库实现者考虑的,这最终转化为日常使用,因为如果要利用它,则需要提供移动构造函数... 在用户代码中,重要的是要知道什么是实际可移动的和什么是不可移动的,而AB不可移动。std::vector是一个移动感知容器,在其中使用A将会复制 - David Rodríguez - dribeas
1
@ildjam - 好的,那我就不是未来的保障了,我很精简。 :-) - Bo Persson
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接