지난 게시글인 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 내부에서 또 하나의 임시 객체를 만든다. 이때 처음 만들어진 임시 객체는 Rvalue이므로 자동적으로 push_back 내부에선 일반 생성자가 아닌 move 생성자를 호출해서 또 다른 임시 객체를 만든다. 이때 처음 만든 임시 객체를 move 생성자를 통해 push_back내부에서 만들어진 임시 객체로 넘겨준다(이동 생성자 호출). 처음 인자로 들어와서 만들어진 임시 객체는 소멸자를 호출해서 사라지고(소멸자 호출), main 종료시 vector 내부의 객체도 사라진다(소멸자 호출). 한번만 만들면 되는 객체를 불필요하게 2번 만들고 있고 그래서 소멸자도 2번이나 호출된다.
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
https://boycoding.tistory.com/208
https://openmynotepad.tistory.com/10
'공부 > C || C++' 카테고리의 다른 글
static 변수와 전역 변수와 비교 (0) | 2022.09.23 |
---|---|
C++17 string_view (2) | 2022.09.19 |
STL Container (시퀀스 컨테이너, 연관 컨테이너, 컨테이너 어댑터) (0) | 2022.09.17 |
C++ std::move (0) | 2022.09.05 |
C++ 11 RVO(Return Value Optimization) (0) | 2022.08.29 |