【C++】いまさらだけどvectorのpush_back()とemplace_back()を比較する

C++

std::vectorに要素を追加する方法にはpush_back()emplace_back()の2つがあります。「何となくemplace_back()の方が速い」というイメージを持っている人は多いと思いますが、実際に何が違うのか、どのくらい差が出るのかを、コードと計測結果を交えて整理します。私の環境下では意外な結果が出ました。

検証環境:

  1. CPU: Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz
  2. メモリ: 16GB
  3. OS: Arch Linux
  4. g++: version 16.2.1
  5. clang++: version 22.1.8

それぞれの定義

まずはcpprefjpベースでの定義を確認します。

  • push_back(const T& x) / push_back(T&& x)
    すでに構築済みのT型オブジェクト(またはそれに変換可能な値)を受け取り、それをvectorの末尾にコピーまたはムーブして格納する。

  • emplace_back(Args&&... args)
    コンストラクタへ渡す引数そのものを受け取り、vectorが確保したメモリ上で Tオブジェクトを直接構築(placement new)する。可変長テンプレートによって任意個の引数を完全転送(perfect forwarding)する。

ポイントは、push_backが「完成品を受け取って配置する」のに対し、emplace_backは「材料(コンストラクタ引数)を受け取ってその場で作る」という点です。この違いが、後述する動作・パフォーマンスの差の根本原因になります。

それぞれの動作

動作確認用のクラスの実装

コンストラクタ・コピーコンストラクタ・ムーブコンストラクタ・デストラクタそれぞれにログを仕込んだクラスを用意し、どの関数が何回呼ばれるかを可視化します。

struct Widget
{
    int id;
    std::string name;

    // 通常のコンストラクタ
    Widget(int id_, std::string name_) : id(id_), name(std::move(name_)) {
        std::cout << "  [Constructor]      id=" << id << " name=" << name << "\n";
    }
    // コピーコンストラクタ
    Widget(const Widget& other) : id(other.id), name(other.name) {
        std::cout << "  [Copy Constructor] id=" << id << " name=" << name << "\n";
    }
    // ムーブコンストラクタ
    Widget(Widget&& other) noexcept : id(other.id), name(std::move(other.name)) {
        std::cout << "  [Move Constructor] id=" << id << " name=" << name << "\n";
    }
    // デストラクタ
    ~Widget() {
        std::cout << "  [Destructor]       id=" << id << " name=" << name << "\n";
    }
};

reserve()で事前に容量を確保し、再配置(reallocation)によるノイズを排除した上で検証します。

push_back() / emplace_back() したときの動作

std::vector<Widget> v;
v.reserve(10);

std::cout << "=== push_back(左辺値) ===\n";
Widget w1(1, "alpha");
v.push_back(w1);

std::cout << "\n=== push_back(右辺値/一時オブジェクト) ===\n";
v.push_back(Widget(2, "beta"));

std::cout << "\n=== emplace_back(引数を直接渡す) ===\n";
v.emplace_back(3, "gamma");

実行結果は次の通りです。

=== push_back(左辺値) ===
  [Constructor]      id=1 name=alpha
  [Copy Constructor] id=1 name=alpha

=== push_back(右辺値/一時オブジェクト) ===
  [Constructor]      id=2 name=beta
  [Move Constructor] id=2 name=beta
  [Destructor]       id=2 name=

=== emplace_back(引数を直接渡す) ===
  [Constructor]      id=3 name=gamma

それぞれの内訳はこうなります。

呼び出し方内部で起きていること
push_back(w1)(左辺値)Widgetコピー構築。元のw1はそのまま残る。
push_back(Widget(2, "beta"))(右辺値)一時オブジェクトをコンストラクタで構築 → vector内へムーブ構築 → 一時オブジェクトを破棄。合計3回の関数呼び出し。
emplace_back(3, "gamma")vectorの生メモリ上にコンストラクタで直接構築。コピーもムーブも発生しない。

つまり emplace_back()は「一時オブジェクトを作ってから移動する」という中間工程を丸ごと省略できるのが最大の特徴です。

{}(波括弧初期化) を渡した場合の挙動差

ここは見落とされがちですが重要な違いです。push_back()は引数を1つ受け取る関数なので、暗黙変換によって波括弧初期化リストをそのまま渡せます。

std::vector<Widget> v;
v.push_back({4, "delta"}); // OK: {4, "delta"} から一時Widgetへ暗黙変換されてpush_backされる

実行結果:

=== push_back({...}) ===
  [Constructor]      id=4 name=delta
  [Move Constructor]

一方 emplace_back()Args&&...という可変長テンプレート引数を取るため、{4, "delta"}のような波括弧初期化リストからは型を推論できず、コンパイルエラーになります。

std::vector<Widget> v;
v.emplace_back({4, "delta"}); // コンパイルエラー

実際にg++でコンパイルすると次のエラーが出ます。

error: no matching function for call to 'std::vector<Widget>::emplace_back(<brace-enclosed initializer list>)'
    v.emplace_back({4, "delta"});
    candidate expects 0 arguments, 1 provided

emplace_back()で同じことをしたい場合は、波括弧を外して引数をそのまま渡す必要があります。

v.emplace_back(4, "delta"); // OK

この違いは、既存コードを push_back({...})emplace_back({...})に単純置換すると壊れる典型的な罠なので覚えておくとよいです。

動作時間の比較

std::stringメンバを持つWidgetを200万回vectorへ追加する処理を、g++clang++-O2最適化ありでベンチマークしました(reserve()済みで再配置は発生しない前提)。数値は10回計測した際の平均値と標準偏差です。

実際に使用したコードは最後に掲載しましたので、そちらをご覧ください。

オブジェクトの場合

呼び出し方g++所要時間clang++所要時間内部動作
push_back(既存の左辺値 w)100.5±4.5 ms100.8±5.12 ms毎回コピーコンストラクタが発生(もっとも遅い)
push_back(Widget(i, name))(一時オブジェクト)65.2±2.39 ms60.4±0.84 ms構築 + ムーブ + 破棄
emplace_back(i, name)81.9±9.11 ms65.5±0.53 msその場で直接構築のみ

左辺値をコピーするケースは、std::stringの内部バッファを丸ごと複製するため明確に遅くなります。一方、右辺値(一時オブジェクト)のpush_back()emplace_back()の比較は意外な結果が出ました。-O2環境では、これは std::stringムーブがポインタの付け替え程度で非常に軽いため、「構築→ムーブ→破棄」という余分な工程があっても、その差が誤差レベルに収まってしまうと予想していたのですが、emplace_back()のほうが遅かったです。ただし、これはg++の生成するコードのせいかもしれません。clang++でコンパイルした場合、両者にはほとんど差がありませんでした。

このことから「emplace_back()は常に速い」というのは正確ではなく、正しく言うなら「emplace_back()は、ムーブコストが高いオブジェクトほど恩恵が大きい」 という理解が実態に近いです。

値の場合

intのようなプリミティブ型をvectorへ追加するケースです。

呼び出し方g++所要時間clang++所要時間
push_back(i)0±0 ms0±0 ms
emplace_back(i)0±0 ms0±0 ms

intはコピーもムーブも「4バイトの値渡し」でしかないため、push_back()emplace_back()の間に実質的な差はありません。int/double/ポインタのようなトリビアルにコピー可能な型では、どちらを使ってもパフォーマンス上の違いはないと考えてよいです。

{}を渡した場合

呼び出し方g++所要時間clang++所要時間
push_back({i, name})62.6±0.84 ms58.9±0.99 ms
emplace_back(i, name)(同等の処理)81.0±9.18 ms68.2±7.33 ms

前述の通り emplace_back({i, name})はそもそもコンパイルできないため、同じ結果を得るには emplace_back(i, name)のように引数を展開する必要があります。計測上はpush_back({...})の方が速いという結果でした。これも意外でした。

少なくとも「{}を使う場面ではemplace_back()が必ず有利」とは言えない、という点は押さえておくべきです。

まとめ

  • 定義: push_back()は完成したオブジェクトを配置し、emplace_back()はコンストラクタ引数からその場でオブジェクトを構築する。
  • 動作: push_back(左辺値)はコピー、push_back(右辺値)は構築+ムーブ+破棄、emplace_back は構築のみ。
  • {}の罠: push_back({...})は暗黙変換で動くが、emplace_back({...})はテンプレート引数推論に失敗しコンパイルエラーになる。
  • 速度: 差が大きく出るのは「既存オブジェクトをコピーする」ケース。ムーブが軽い型(std::stringなど)の一時オブジェクトを渡す場合や、intのようなプリミティブ型では両者の差はほぼない。

「とりあえずemplace_back()にしておけば速い」ではなく、「emplace_back()は不要なコピー/ムーブの可能性を構造的に排除できる」という理解で使い分けるのが良いと思います。可読性の観点からは、単に既存オブジェクトを追加するだけならpush_back()の方が意図が明確な場合も多く、常にemplace_back()一択にする必要はありません。

付録: ベンチマークコード全文

実際に使用した計測コードの全体です。ビルドするには以下のコマンドを実行してください。

  1. g++ -std=c++17 -O2 benchmark.cpp -o benchmark
  2. clang++ -std=c++17 -O2 benchmark.cpp -o benchmark

reserve(N)で事前に容量を確保しているため、計測中に再配置(reallocation)は発生しません。

#include <chrono>
#include <iostream>
#include <string>
#include <vector>

// ある程度コストのかかるコンストラクタ・コピー・ムーブを持つクラス
struct Widget
{
    int id;
    std::string name; // ヒープ確保を伴う典型的なメンバ

    Widget(int id_, std::string name_) : id(id_), name(std::move(name_)) {}
    Widget(const Widget& other) : id(other.id), name(other.name) {}
    Widget(Widget&& other) noexcept : id(other.id), name(std::move(other.name)) {}
};

constexpr int N = 2000000;

template <class F>
long long measure(F&& f)
{
    auto start = std::chrono::high_resolution_clock::now();
    f();
    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}

int main()
{
    // -------------------------------------------------
    // 1. オブジェクトを渡す場合
    // -------------------------------------------------
    {
        // 既存の左辺値をpush_back -> 毎回コピーコンストラクタが走る最悪ケース
        std::vector<Widget> v;
        v.reserve(N);
        Widget w(0, "some_reasonably_long_name_for_sso_bypass");
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.push_back(w);
            }
        });
        std::cout << "push_back(既存の左辺値w)   : " << ms << " ms\n";
    }
    {
        std::vector<Widget> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.push_back(Widget(i, "some_reasonably_long_name_for_sso_bypass"));
            }
        });
        std::cout << "push_back(Widget(...))     : " << ms << " ms\n";
    }
    {
        std::vector<Widget> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.emplace_back(i, "some_reasonably_long_name_for_sso_bypass");
            }
        });
        std::cout << "emplace_back(i, name)      : " << ms << " ms\n";
    }

    // -------------------------------------------------
    // 2. 値(int)を渡す場合。std::vector<int>ではpush_backとemplace_backに差が出にくい例
    // -------------------------------------------------
    {
        std::vector<int> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.push_back(i);
            }
        });
        std::cout << "push_back(int)             : " << ms << " ms\n";
    }
    {
        std::vector<int> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.emplace_back(i);
            }
        });
        std::cout << "emplace_back(int)          : " << ms << " ms\n";
    }

    // -------------------------------------------------
    // 3. {}(波括弧初期化)を渡す場合。push_backのみ計測(emplace_backはコンパイル不可のため対象外)
    // -------------------------------------------------
    {
        std::vector<Widget> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.push_back({i, "some_reasonably_long_name_for_sso_bypass"});
            }
        });
        std::cout << "push_back({i, name})       : " << ms << " ms\n";
    }
    {
        // emplace_backで同等の処理をする場合は引数を展開して渡す
        std::vector<Widget> v;
        v.reserve(N);
        auto ms = measure([&] {
            for (int i = 0; i < N; ++i) {
                v.emplace_back(i, "some_reasonably_long_name_for_sso_bypass");
            }
        });
        std::cout << "emplace_back(i, name) [同等]: " << ms << " ms\n";
    }

    return 0;
}

実行結果の一例(3回実施)

  1. g++でコンパイルした結果

    push_back(既存の左辺値w)   : 101 ms
    push_back(Widget(...))     : 68 ms
    emplace_back(i, name)      : 78 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 63 ms
    emplace_back(i, name) [同等]: 78 ms
    
    push_back(既存の左辺値w)   : 98 ms
    push_back(Widget(...))     : 64 ms
    emplace_back(i, name)      : 79 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 63 ms
    emplace_back(i, name) [同等]: 78 ms
    
    push_back(既存の左辺値w)   : 101 ms
    push_back(Widget(...))     : 64 ms
    emplace_back(i, name)      : 78 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 64 ms
    emplace_back(i, name) [同等]: 77 ms
  2. clang++でコンパイルした結果

    push_back(既存の左辺値w)   : 96 ms
    push_back(Widget(...))     : 63 ms
    emplace_back(i, name)      : 65 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 58 ms
    emplace_back(i, name) [同等]: 65 ms
    
    push_back(既存の左辺値w)   : 112 ms
    push_back(Widget(...))     : 60 ms
    emplace_back(i, name)      : 65 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 58 ms
    emplace_back(i, name) [同等]: 65 ms
    
    push_back(既存の左辺値w)   : 97 ms
    push_back(Widget(...))     : 60 ms
    emplace_back(i, name)      : 65 ms
    push_back(int)             : 0 ms
    emplace_back(int)          : 0 ms
    push_back({i, name})       : 58 ms
    emplace_back(i, name) [同等]: 65 ms

C++

Posted by izadori