【问题标题】:Most efficient way to return+reset member variable?返回+重置成员变量的最有效方法?
【发布时间】:2016-03-20 23:47:48
【问题描述】:

以下实现GetDeleteObjects 的最有效方法是什么?

class Foo {
public:
  std::vector<Bar> GetDeleteObjects();
private:
  std::vector<Bar> objects_;
}

std::vector<Bar> Foo::GetDeleteObjects() {
  std::vector<Bar> result = objects_;
  objects_.clear();
  return result;
}

目前,至少执行了从 objects_ 到 result 的复制。例如,使用std::move 可以加快这一速度吗?

【问题讨论】:

  • C++14: return std::exchange(objects_, {});

标签: c++ c++11 copy move-semantics


【解决方案1】:

你可以交换向量:

std::vector<Bar>
Foo::GetDeleteObjects() {
  std::vector<Bar> result;
  result.swap(objects_);
  return result;
}

【讨论】:

  • 正如@Richard Hodges 所表明的那样,这个解决方案和std::move 的解决方案在性能方面是相当的,我更喜欢这个解决方案,因为我觉得它更直观一点。值得注意的是,这也将objects_ 的容量重置为 0(使用带有空向量的swap 是我所知道的重置容量的规范方法),所以如果你希望你会添加很多项目再次发送到objects_,您可能需要先存储容量并确保在返回之前reserve 该空间。
【解决方案2】:

您可以将移动构造用于移动感知类型,例如std::vector&lt;T&gt;

std::vector<Bar>
Foo::GetDeleteObjects() {
     std::vector<Bar> result(std::move(objects_));
     objects_.clear(); // objects_ left in unspecified state after move
     return result;
}

移动构造期间的传输很可能已经重置了指针,clear() 不会做任何事情。由于无法保证从对象移动的状态是什么,不幸的是,clear() 是必需的。

【讨论】:

  • 这是否保证工作(对于任何移动感知对象)?通常,移动用于临时对象,这些对象将立即被删除,即唯一的保证是您可以破坏它们。在您的示例中,您使用为std::vector 定义的clear(),但不是通用的。所以,我认为您的解决方案适用于std::vector,但不适用于一般的移动感知类型(与您所说的相反)。
  • @Walter:移动作品一般是为了转移物体。原始状态确实取决于实现,这可能会提供比标准 C++ 库所做的更强或更弱的保证。对于标准 C++ 库类,从对象移出的对象保留在有效但未指定的状态中,因此必须使用 clear()。对于其他类型,可能需要使用不同的方法将已移动的对象恢复到已知状态。移动“通常”用于临时对象是编译器隐含地知道它可以从这些移动,否则它需要显式。
  • @Walter:我不会做出这样的假设。最后,我克服了一个错误的投票是有意义的想法。另外,我已经拿到了 Carl Fredricksen 的帽子——所以我不介意 :-)
  • 为了胡安的利益,值得一提的是,重要的是编写正确的代码,清楚地表达意图。在几乎所有情况下,优化器都会比您更有效地实现您的意图。例如,在我使用的所有实现中,clear() 中的代码将被优化掉 - 但它必须保留才能使代码正确。
  • @JanRüegg:当遗留对象的状态可以未指定时,我希望它比swap() 更快。当需要将原始值重置为定义的值时,事情就变得不太清楚了。这两个版本中哪个更快可能需要测量。不过,假设移动/clear() 和临时/swap() 的大型向量将比创建要返回的副本更快。
【解决方案3】:

其他三个答案是正确的,所以在回答问题方面我没有什么要补充的,但由于 OP 对效率感兴趣,我用 -O3 编译了所有建议。

这两种解决方案之间几乎没有任何区别,但std::exchange 解决方案在我的编译器上生成更高效的代码,具有额外的优势,即它的惯用完美。

我认为结果很有趣:

给定:

std::vector<Bar> Foo::GetDeleteObjects1() {
    std::vector<Bar> tmp;
    tmp.swap(objects_);
    return tmp;
}

结果:

__ZN3Foo17GetDeleteObjects1Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movq    $0, 8(%rdi)          ; construct tmp's allocator
    movq    $0, (%rdi)           ;... shame this wasn't optimised away
    movups  (%rsi), %xmm0        ; swap
    movups  %xmm0, (%rdi)
    xorps   %xmm0, %xmm0         ;... but compiler has detected that
    movups  %xmm0, (%rsi)        ;... LHS of swap will always be empty
    movq    16(%rsi), %rax       ;... so redundant fetch of LHS is elided
    movq    %rax, 16(%rdi)
    movq    $0, 16(%rsi)         ;... same here
    movq    %rdi, %rax
    popq    %rbp
    retq

给定:

std::vector<Bar>
Foo::GetDeleteObjects2() {
    std::vector<Bar> tmp = std::move(objects_);
    objects_.clear();
    return tmp;
}

结果:

__ZN3Foo17GetDeleteObjects2Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp3:
    .cfi_def_cfa_offset 16
Ltmp4:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp5:
    .cfi_def_cfa_register %rbp
    movq    $0, 8(%rdi)         ; move-construct ... shame about these
    movq    $0, (%rdi)          ; ... redundant zero-writes
    movups  (%rsi), %xmm0       ; ... copy right to left ...
    movups  %xmm0, (%rdi)
    movq    16(%rsi), %rax
    movq    %rax, 16(%rdi)
    movq    $0, 16(%rsi)      ; zero out moved-from vector ...
    movq    $0, 8(%rsi)       ; ... happens to be identical to clear()
    movq    $0, (%rsi)        ; ... so clear() is optimised away
    movq    %rdi, %rax    
    popq    %rbp
    retq

最后,给定:

std::vector<Bar>
Foo::GetDeleteObjects3() {
    return std::exchange(objects_, {});
}

结果非常令人愉快:

__ZN3Foo17GetDeleteObjects3Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp6:
    .cfi_def_cfa_offset 16
Ltmp7:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp8:
    .cfi_def_cfa_register %rbp
    movq    $0, (%rdi)            ; move-construct the result
    movq    (%rsi), %rax
    movq    %rax, (%rdi)
    movups  8(%rsi), %xmm0
    movups  %xmm0, 8(%rdi)
    movq    $0, 16(%rsi)          ; zero out the source
    movq    $0, 8(%rsi)
    movq    $0, (%rsi)
    movq    %rdi, %rax
    popq    %rbp
    retq

结论:

std::exchange 方法在惯用上完美且效率最佳。

【讨论】:

    【解决方案4】:

    惯用的表达方式是使用std::exchange(C++14 起):

    std::vector<Bar> Foo::GetDeleteObjects() {
      return std::exchange(objects_, {});
    }
    

    请注意,这假设分配一个值初始化的vector 等价于调用clear;除非您使用带有propagate_on_container_move_assignment 的有状态分配器,否则会出现这种情况,在这种情况下,您需要明确地重用分配器:

    std::vector<Bar> Foo::GetDeleteObjects() {
      return std::exchange(objects_, std::vector<Bar>(objects_.get_allocator()));
    }
    

    【讨论】:

    • 有趣的是,这也产生了最有效的 clang 代码。我会相应地更新我的答案。赞成。
    【解决方案5】:

    更新:
    理查德是对的。在查看std::move 的定义后,它处理的是指针而不是实际值,这比我想象的要聪明。所以下面的技术已经过时了。

    旧(过时):
    你可以使用指针

    class Foo {
    public:
      Foo();
      std::vector<Bar> GetDeleteObjects();
    
    private:
      std::vector<Bar> objects1_;
      std::vector<Bar> objects2_;
    
      std::vector<Bar> *currentObjects_;
      std::vector<Bar> *deletedObjects_;
    }
    
    Foo::Foo() :
      currentObjects_(&objects1_)
      , deletedObjects_(&objects2_)
    {
    }
    
    Foo::GetDeleteObjects() {
      deletedObjects_->clear();
    
      std::swap(currentObjects_, deletedObjects_);
    
      return *deletedObjects;
    }
    

    【讨论】:

    • std::move 已经过时了这种技术。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-30
    • 2021-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-28
    相关资源
    最近更新 更多