为什么emplace_back比push_back更快?

48

当我这样做时,我认为emplace_back应该是最佳选择:

v.push_back(myClass(arg1, arg2));

因为emplace_back会立即在向量中构造对象,而push_back会先构造一个匿名对象,然后将其复制到向量中。更多信息请参见问题。

Google还提供了这个这个问题。

我决定比较它们用于填充整数的向量。

下面是实验代码:

#include <iostream>
#include <vector>
#include <ctime>
#include <ratio>
#include <chrono>

using namespace std;
using namespace std::chrono;

int main() {

  vector<int> v1;

  const size_t N = 100000000;

  high_resolution_clock::time_point t1 = high_resolution_clock::now();
  for(size_t i = 0; i < N; ++i)
    v1.push_back(i);
  high_resolution_clock::time_point t2 = high_resolution_clock::now();

  duration<double> time_span = duration_cast<duration<double>>(t2 - t1);

  std::cout << "push_back took me " << time_span.count() << " seconds.";
  std::cout << std::endl;

  vector<int> v2;

  t1 = high_resolution_clock::now();
  for(size_t i = 0; i < N; ++i)
    v2.emplace_back(i);
  t2 = high_resolution_clock::now();
  time_span = duration_cast<duration<double>>(t2 - t1);
  std::cout << "emplace_back took me " << time_span.count() << " seconds.";
  std::cout << std::endl;

  return 0;
}

结果是emplace_back更快。

push_back took me 2.76127 seconds.
emplace_back took me 1.99151 seconds.

为什么?第一个链接问题的答案清楚地表明不会有性能差异。

我还尝试了其他时间方法,但结果相同。

[编辑] 评论说,测试int并没有什么意义,并且push_back需要引用。

我在上面的代码中进行了相同的测试,但是我使用了类A而不是int:

class A {
 public:
  A(int a) : a(a) {}
 private:
  int a;
};

结果:

push_back took me 6.92313 seconds.
emplace_back took me 6.1815 seconds.

[编辑.2]

正如denlan所说,我也应该改变操作的顺序,所以我交换了它们的位置,在两种情况下(intclass A),emplace_back再次获胜。

[解决方案]

我是在debug模式下运行代码,这使得测量结果无效。进行基准测试时,始终以release模式运行代码。


那么,这是否意味着在发布模式下使用时您不会看到这些性能差异?您有相同的数字吗? - talekeDskobeDa
嘿@talekeDskobeDa,我在我的旧笔记本电脑上执行这段代码,所以不好意思,无法提供给你。 - gsamaras
我明白在原则上emplace_back会更快 - 肯定不会更慢。但我不明白(也找不到任何地方)为什么即使在这种最简单的情况下,优化器也不能生成相同的代码。有什么想法吗? - Ben
不是真的@Ben,但如果你愿意,你可以发布一个新问题,链接到我的问题,询问确切的内容(如果你分享给我,请告诉我)。 :) - gsamaras
2个回答

58
你的测试用例并不是很有帮助。push_back接受一个容器元素并将其复制/移动到容器中。emplace_back接受任意参数,并从这些参数构造一个新的容器元素。但如果你传递一个已经是元素类型的单个参数给emplace_back,那么你仍然会使用复制/移动构造函数。
下面是更好的比较:
Foo x; Bar y; Zip z;

v.push_back(T(x, y, z));  // make temporary, push it back
v.emplace_back(x, y, z);  // no temporary, directly construct T(x, y, z) in place

然而,关键的区别在于emplace_back执行显式转换:

std::vector<std::unique_ptr<Foo>> v;
v.emplace_back(new Foo(1, 'x', true));  // constructor is explicit!

在未来,这个例子可能会稍微有些牵强,但你应该这样说:v.push_back(std::make_unique<Foo>(1, 'x', true))。然而,其他构造函数也可以很好地使用emplace

std::vector<std::thread> threads;
threads.emplace_back(do_work, 10, "foo");    // call do_work(10, "foo")
threads.emplace_back(&Foo::g, x, 20, false);  // call x.g(20, false)

我认为你的 v.emplace_back(new Foo(1, 'x', true)); 不是异常安全的,因为它创建了新的 Foo 再调用 emplace_back 可能会抛出异常,导致 Foo 泄漏。 - Ben
1
@Ben:没错。因此使用std::make_unique,以允许安全且不太冗长的解决方案。 - Kerrek SB

2
首先,针对那个问题我的答案是:emplace 成员函数并不总是比 push 成员函数更快,有时它们的效率是相同的
根据我从《C++ Primer》中了解到的关于emplacepush成员函数之间的区别,前者包括emplace_back是用于将参数传递给元素类型的构造函数。这意味着emplace成员直接使用这些参数在容器管理的空间中构造一个元素。但是在这种情况下,需要注意的是,只有当给定的参数需要隐式转换为元素类型时,或者说,给定的参数不是元素类型但与之“兼容”时,才会发生容器元素的直接初始化。否则会调用复制/移动构造函数,其“代价”应该与调用push成员相同。相反,当调用push成员时,我们必须传递元素类型的对象,然后将这些对象复制到容器中。因此,在这种情况下,复制初始化中特定于push成员的额外复制步骤应该需要更多时间。 类似于yizzlez提供的比较示例,这里是我简单的例子,说明我提到的区分emplace_backpush_back的两种情况:
class T 
{
public:
  T()=default;
  explicit T(const std::string& rstr) : str(rstr) { }
  T(const T&);  //the same as synthesized copy constructor
private:
  std::string str;
}

const std::string test = "testing";
std::vector<T> vec_t;
// better executing efficiency achieved by emplace_back
vec_t.emplace_back(test);  // explicit constructor is called behind
vec_t.push_back(T(test));
// the same executing efficiency
vec_t.emplace_back(T(test));  // copy constructor is called behind
vec_t.push_back(T(test));

因此,类似于yizzlez提供的第一个示例,相应的emplace_back方法可以产生更简洁且可能更高效的代码,当需要在调用push_back方法时显式构造容器元素类型的临时对象时,您可以在这个更具体的link中查看。

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