공부/C || C++

C++ vector::push_back vs vector::emplace_back

sudo 2022. 9. 18. 20:15

지난 게시글인 Rvalue reference관해서 공부하다가 우연히 보게 된 건데 C++에 container들을 자주 써왔다고 생각했는데 push_back과 emplace_back에 performance 차이가 있을 수 있다는 이야기는 처음 들어봤다. 사실 emplace_back은 거의 안써보고 매번 push_back만 쓰기도 했다. 겉보기에 두개의 메소드 간에 차이가 없어보이는데 왜 다른 이름으로 두개가 존재하는건지 진작에 의문점을 가져봤어야 했는데...

 

push_back과 emplace_back의 가장 큰 차이점은 "emplace_back은 내부적으로 가변 인자 템플릿 형태의 생성자가 구현되어 있다는 점이다" 그래서 결과적으로 두 함수에 생기는 차이는

  •  push_back은 내부적으로 생성자가 없어서 외부에서 임시 객체를 만들고 내부로 복사 해주고, 외부에 생성된 임시 객체는 소멸된다
  • emplace_back은 내부적으로 생성자가 있기 때문에 내부에서 생성자를 통해 자체적으로 객체를 생성

 

push_back은 모두 다 알듯이 vector의 끝에 element를 추가하는 것이고 C++ 11부터 원형은 아래와 같다.

void push_back (const value_type& val);
void push_back (value_type&& val);

두번째줄을 보면 value_type&& val에 이전에 공부했던 Rvalue reference가 보인다. 그래서 지난 게시글(https://welikecse.tistory.com/1)에서 볼 수 있었듯이, move 연산자가 정의 되어 있고, push_back에 아래와 같이 Rvalue인 임시객체를 넣어주면 push_back의 두번째 함수가 호출되게 될 것이다.

vector<MemoryBlock> v;
v.push_back(MemoryBlock(25));

 

(만약 move 연산자가 정의되지 않았을 때 위처럼 Rvalue를 인자로 넘겨주는 push_back을 호출하면 복사 생성자가 호출될 수 있는 이유는 찾아 봤다. 복사 생성자의 파라미터는

MemoryBlock(const MemoryBlock& other)

이렇게 파라미터가 const reference 형식일텐데 const reference는 non-const lvalue, const lvalue 및 r-value로 초기화할 수 있다고 한다. 예를 들면 아래와 같다(코드와 위의 정보 출처는 https://boycoding.tistory.com/208)

int x = 5; 
const int& ref1 = x; // okay, x is a non-const l-value
const int y = 7; 
const int& ref2 = y; // okay, y is a const l-value
const int& ref3 = 6; // okay, 6 is an r-value

 

emplace_back또한 vector의 끝에 element를 추가하고 원형은 아래와 같다.

template< class... Args >
void emplace_back( Args&&... args );

원형부터 push_back과 다른 점은 가변 인자 템플릿(variadic template)이 사용됐다는 점이다. 가변 인자 템플릿에 대한 내용은 다음 게시물을 참고하자.

https://welikecse.tistory.com/4

 

코드 예시를 보자 (코드 출처: https://openmynotepad.tistory.com/10)

class Item {
public:

 Item(const int _n) : m_nx(_n) { cout << "일반 생성자 호출" << endl; }

 Item(const Item& rhs) : m_nx(rhs.m_nx) { cout << "복사 생성자 호출" << endl; }

 Item(const Item&& rhs) : m_nx(std::move(rhs.m_nx)) { cout << "이동 생성자 호출" << endl; }

 ~Item() { cout << "소멸자 호출" << endl; }
 
 private:
 int m_nx;
};

int main() {
 std::vector<Item> v;

 cout << "push_back 호출" << endl;
 v.push_back(Item(3));
 
 cout << "emplace_back 호출" << endl;
 v.emplace_back(3);

} 

 

맨 처음 코드를 보았을 때 눈에 띄는 점은 push_back은 Item(3) 처럼 임시 객체를 인자로 주는 반면에, emplace_back은 3만 넘겨준다는 점이다. emplace_back은 내부적으로 가변 인자 템플릿으로 구현된 생성자가 있어서, 객체 생성에 필요한 인자만 넘겨줘도 내부적으로 객체를 생성하고 그렇기 때문에 함수 외부에서 내부로 복사를 해주지 않아도 되고, 외부에서 생긴 객체 소멸자도 호출할 필요가 없다. 이런 과정 때문에 두개 함수간에 performance 차이를 보인다.

 

출력한 결과도 다른데, 위의 코드에서 push_back만 호출하면

push_back 호출 결과

이런 결과가 나온다. 인자를 들어온걸 임시 객체로 생성자로 만들고(일반 생성자 호출), push_back 내부에서 또 하나의 임시 객체를 만든다. 이때 처음 만들어진 임시 객체는 Rvalue이므로 자동적으로 push_back 내부에선 일반 생성자가 아닌 move 생성자를 호출해서 또 다른 임시 객체를 만든다. 이때 처음 만든 임시 객체를 move 생성자를 통해 push_back내부에서 만들어진 임시 객체로 넘겨준다(이동 생성자 호출). 처음 인자로 들어와서 만들어진 임시 객체는 소멸자를 호출해서 사라지고(소멸자 호출), main 종료시 vector 내부의 객체도 사라진다(소멸자 호출). 한번만 만들면 되는 객체를 불필요하게 2번 만들고 있고 그래서 소멸자도 2번이나 호출된다.

 

emplace_back만 호출하면

emplace_back 호출 결과

내부에서 자체적으로 임시 객체로 만들고(일반 생성자 호출) 그것을 바로 vector에 삽입하고, main 종료시 소멸자가 호출된다.단 여기서 emplace_back도 push_back처럼 인자로 임시 객체 Item(3)을 넘겨도 되긴 된다.

v.emplace_back(Item(3));

 그런데 이렇게 하면 결국

이렇게 push_back과 다를바가 없다. "결국 임시 객체를 만들어서 인자로 넘기는게 아니라 emplace_back에 필요한 인자만 넘기는게 두 함수간의 performance 차이를 만드는 것이다". 그리고 당연한 이야기지만 emplace_back에 lvalue를 넘기면 push_back과 똑같은 결과가 나온다.

 

그럼 performance에는 얼마나 차이가 있을지 100000번 반복해서 단순히 프로그램 실행 시간의 차이를 살펴보는 실험을 해보자.

프로그램 실행 시간 차이를 보자

push_back의 경우 241~289ms 정도 걸렸고, emplace_back의 경우 219~255ms 정도 소요됐다.

 

하지만 반드시 emplace_back이 좋은 것은 아니며, 컴파일러 최적화로 거의 차이가 없는 경우도 많다고 한다.

 

 

 

Reference

https://shaeod.tistory.com/630

 

[C++ STL] std::vector - emplace_back

※ 요약 std::vector의 멤버 함수인 emplace_back은 C++11부터 추가된 멤버 함수로써 push_back과 같이 vector의 요소 끝에 원소를 추가하는 함수이다. 두 함수의 가장 큰 차이점은, push_back과 같은 삽입 함수..

shaeod.tistory.com

https://boycoding.tistory.com/208

 

C++ 07.16 - 참조와 const (Reference and const)

참조와 const (Reference and const) Reference to const value const 값에 대한 포인터를 선언하는 것처럼 const 값에 대한 참조를 선언할 수 있다. const 키워드를 사용하여 참조를 선언하면 된다. cons..

boycoding.tistory.com

https://openmynotepad.tistory.com/10

 

emplace_back 과 push_back 의 차이

item 타입의 생성자가 타입을 인자로 받는다면? push_back 함수는 '객체' 를 집어 넣는 형식으로, 객체가 없이 삽입을 하려면 "임시객체 (rvalue) " 가 있어야 합니다. 또는 암시적 형변환이 가능하다면,

openmynotepad.tistory.com