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

时间:2011-05-03 06:32:25

标签: c++ optimization c++11

我已经问了一些涉及这个问题的问题,但我得到了不同的回答,所以我最好直接问它。

假设我们有以下代码:

// 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的位置创建并传入值,而不是创建A和B的调用约定以及对它们的引用。使用NRVO,这将意味着创建并将它们直接传递到x,而create_c函数无需进一步处理。

这样可以避免创建A和B的副本。

有没有办法允许/鼓励/强制这种行为来自编译器,或者优化编译器通常会这样做吗?并且这仅在编译器内联函数时才有效,或者它是否可以在编译单元之间工作。

(我认为这可以在编译单元中起作用......)

如果create_a()create_b()采用隐藏参数放置返回值的位置,则可以将结果直接放入x,然后通过引用传递给{{1}这需要什么都不做,并立即返回。

3 个答案:

答案 0 :(得分:4)

优化代码有不同的方法,但右值引用不是一个。问题是,AB都不能免费移动,因为您无法窃取对象的内容。请考虑以下示例:

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,这样原始对象析构函数就不会释放内存。

AB的问题在于实际内存是通过数组对象中保存的,并且该数组不能移动到新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动态分配的C* p = new C; p->a = create_a();对象的情况下,无法完全使用此优化,因为目标不是 in在堆栈中,编译器只能优化临时内部create_a及其返回值,但它不能与p->a重合,因此需要完成一个副本。这是 rvalue-references 优于(N)RVO的优势,但如前所述,您无法直接在代码示例中有效地使用 rvalue-references

答案 1 :(得分:3)

有两种优化可以适用于您的情况:

  1. Function Inlining(在A,B和C的情况下(包含A和B))
  2. Copy elision(仅限C(及其包含的A和B),因为您按值返回C)
  3. 对于这个小的函数,它可能会被内联。如果它存在于同一个翻译单元中,大多数编译器都会这样做,而像MSVC ++和G ++这样的好编译器(我认为LLVM,但我不确定那个)有完整的程序优化设置,甚至可以实现翻译单位。如果函数是内联的,那么是的,函数调用(以及它附带的副本)根本不会发生。

    如果由于某种原因该函数没有内联(即你在MSVC ++上使用__declspec(noinline)),那么你仍然有资格获得Named Return Value Optimization (NRO),这是一个优秀的C ++编译器(再次,MSVC ++,G ++,我认为LLVM)都实现了。基本上,标准规定编译器如果可以避免这样做,就不允许在返回时执行复制,并且它们通常会发出避免它的代码。您可以采取一些措施来停用NRVO,但在大多数情况下,这是一个非常安全的优化依赖。

    最后,个人资料。如果你看到性能问题,那么找出其他的东西。否则,我会以意识形态的方式编写内容,并且只在您需要的时候用更高效的结构替换它们。

答案 2 :(得分:0)

给C构造函数然后说:

是不是显而易见的事情
C create_c(const A & a, const B & b)
{
  return C( a, b );
}

有很多优化的可能性。或者确实摆脱了创造功能。我不认为这是一个非常好的激励例子。