RVO
- 임시 객체로의 복사 및 이동 생성자의 호출을 생략해서 최적화하는 방식이다. 특히 함수의 return문과 관계가 깊다.
예시 코드를 보자
struct Snitch
{ // Note: All methods have side effects
Snitch() { cout << "c'tor" << endl; }
~Snitch() { cout << "d'tor" << endl; }
Snitch(const Snitch&) { cout << "copy c'tor" << endl; }
Snitch(Snitch&&) { cout << "move c'tor" << endl; }
Snitch& operator=(const Snitch&)
{
cout << "copy assignment" << endl;
return *this;
}
Snitch& operator=(Snitch&&)
{
cout << "move assignment" << endl;
return *this;
}
Snitch ExampleRVO()
{
return Snitch();
}
int main()
{
Snitch snitch = ExampleRVO();
}
기존의 배운 지식으로 콘솔창 출력을 예상해보면 다음과 6줄과 같다.
1. c'tor
- ExampleRVO 안에서 Snitch 생성자 호출
2. move c'tor
- 함수 ExampleRVO의 리턴 타입이 Reference가 아니므로 Move Constructor 호출로 얕은 복사를 하고 소유권을 이전
3. d'tor
- 소유권을 리턴될 예정인 객체에 이전했으면 자신은 소멸
4. move c'tor
- main 함수 안에서 지역 변수 snitch에 함수 ExampleRVO()의 리턴값인 임시 객체가 Move 되어서 Move Constructor 호출
5. d'tor
- 바로 위에서 변수 snitch에 move해주고 본인은 소멸
6. d'tor
- 지역변수 snitch 소멸
하지만 결과는 이렇게 콘솔창에 두 줄 밖에 안나온다
RVO 때문에 쓸 데 없이 임시 객체에 복사되거나 move되는 것을 생략하도록 최적화됐기 때문이다.
위의 결과를 보면 2, 3, 4, 5줄이 생략됐다. 2~5 내용을 보면 임시 객체가 전달되고 본인은 소멸되는 내용이다. RVO는 이렇게 Src가 임시 객체일 때 Dest에 복사나 Move를 해줘야할 때 복사나 Move를 하지 않고, Dest에 직접 써주자! 라는게 주된 골자다. RVO보다 더 넓은 개념인 Copy Elision(복사 생략)은 RVO를 포함하는 개념이다. 조금 더 고급스럽게 설명하신 블로그에서 정의를 인용해보자
- 어떤 reference에도 bound되지 않은 nameless temporary 가 복사/이동될 때, 복사/이동 생성자가 생략되고 대신 복사/이동하려는 공간에 직접(바로) 할당됨. nameless temporary 가 return 문의 argument인 경우, 이러한 copy elision을 RVO라 부른다.
그런데 RVO를 공부하면서 추가적으로 알게 된 특이한 경우가 있었다. 다음 코드를 보자
class A
{
public:
A() { cout << "Cons" << endl; }
A(A& a) { cout << "Copy &" << endl; }
A(A&& a) { cout << "Move &&" << endl; }
~A() { cout << "Dest" << endl; }
A& operator=(A&& a) noexcept { cout << "Move Assignment &&" << endl; return *this; }
};
A F1(A a)
{
cout << "F1" << endl;
return a;
}
A F2(A& a)
{
cout << "F2" << endl;
return a;
}
int main()
{
A a;
cout << "--------------" << endl;
A b = a;
cout << "--------------" << endl;
A c = F1(b);
cout << "--------------" << endl;
A d = F2(c);
cout << "--------------" << endl;
return 0;
}
출력 결과는 다음과 같다
먼저 F1 호출로 인해 생긴 출력은 콘솔창에 줄로 나누어진 Block중에 4줄로 구성된 3번째 Block이다. 위 코드 예시가 조금 특이한 이유는 매개 변수는 RVO 대상이 아니라 최적화 되지 않지만, 그 매개 변수를 값으로 리턴( = 레퍼런스X)해서 복사된 임시 객체에는 RVO가 적용될 수 있다. 그래서 F1 호출의 경우에도 매개 변수 자체에는 RVO가 적용되지 않았지만 리턴된 결과를 객체 c에 복사해줄때는 RVO가 적용된 결과다.
Copy &
- 매개변수가 Call by Value 방식으로 전달돼서 복사 생성자 호출
F1
- cout으로 F1 출력
Move && 와 Dest
- F1(b)내부에 리턴될 객체 a는 레퍼런스가 아닌 형태로 리턴될 것 이므로 매개 변수로 받은 a를 리턴될 객체 a에
Move 생성자로 move하고 자신은 소멸된다.
하지만 F1(b)라는 임시 객체가 객체 c에 move되고 우변의 임시 객체는 소멸되는 부분은 RVO로 최적화되어서
만약 F1 호출 부분이 이렇게 수정되었다고 생각해보자
A c;
c = F1(b);
그렇다면 출력 결과는 이렇게 될 것이다
생성자, Move Assignment Operator와 소멸자 호출이 추가된 것을 확인할 수 있다. 생성을 따로 처음에 하니 생성자 호출은 당연하고 나머지 두 함수 호출은 기존 코드에서 RVO로 생략되도록 최적화됐었던 것이다.
수정된 코드는 복사 생성자가 호출되는 경우에 해당되지 않으므로 c = F1(b); 코드에서 복사 생성자가 호출되지 않아서 RVO 최적화 조건을 만족하지 않는 것이다(원래 코드에선 객체의 생성과 초기화를 동시에 해서 복사 생성자를 호출하는 조건을 만족하지만 수정된 코드에서 c 객체는 새로 생성된게 아니라 생성과 초기화를 따로 하고 있어서 복사 생성자가 호출되지 않는다. 그리고 RVO 자체가 적용되는 경우는 return 될 객체가 복사되거나 암묵적으로 std::move가 적용될 때 뿐이다[3])
*참고 사항
복사 생성자 호출 시기
1. 기존에 생성된 객체로 새로 생성된 객체를 초기화 할 때
2. 매개 변수를 Call by Value로 받을 때
3. 리턴 타입이 레퍼런스가 아닐 때
아래 참고 자료에서 확인할 수 있듯이 함수 매개 변수는 RVO 대상이 아니다.[1]
함수 F2의 경우도 이어서 보자. 함수 F2의 호출로 생긴 출력은 2줄뿐이다.
F2
- cout 출력
Copy &
- F2는 선언된 리턴 타입은 레퍼런스가 아니지만 인자는 레퍼런스로 받아서 리턴하기 때문에 Move Constructor가 아닌 Copy Constructor를 호출하고 있다.
마찬가지로 F2(c)라는 임시 객체가 객체 d에 move되는 경우는 또 RVO가 적용된 것이다.
F2 함수 호출도 위의 F1 함수 경우와 마찬가지로 아래 처럼 코드를 수정하면 RVO가 적용되지 않을 것이다.
A d;
d = F2(c);
출력 결과는 아래와 같을 것이다
생성자와, Move 대입 연산자, 소멸자 호출이 추가된 것을 확인할 수 있다. 위의 수정된 F1 경우와 마찬가지로 생성을 따로 하니 맨 처음 생성자 호출이 추가되는건 당연하다. Move 대입 연산자와 소멸자가 생긴거도 RVO가 적용 안됐으니 당연히 생략되지 않고 호출되는 것을 확인할 수 있다.
Reference
[1] https://shaharmike.com/cpp/rvo/#performance
[2] https://hyo-ue4study.tistory.com/346
[4] https://devwoodo.blogspot.com/2016/06/copy-elision-rvo-nrvo.html
'공부 > C || C++' 카테고리의 다른 글
STL Container (시퀀스 컨테이너, 연관 컨테이너, 컨테이너 어댑터) (0) | 2022.09.17 |
---|---|
C++ std::move (0) | 2022.09.05 |
const 멤버 함수의 리턴 타입을 레퍼런스로 할 수 없는 이유/ const 멤버함수 내부에서 const가 아닌 멤버 함수를 호출할 수 없다 (0) | 2021.11.12 |
C++ const가 붙는 위치에 따른 의미변화 (0) | 2021.09.01 |
C++ 변환 연산자(Conversion Operator) (0) | 2021.08.28 |