공부/C || C++

C++ 11 RVO(Return Value Optimization)

sudo 2022. 8. 29. 20:40

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

 

Return Value Optimization | Shahar Mike's Web Spot

Return Value Optimization (1096 words) Fri, Aug 18, 2017 Return Value Optimization (RVO), Named RVO (NRVO) and Copy-Elision are in C++ since C++98. In this post I will explain what these concepts mean and how they help improve runtime performance. I will u

shaharmike.com

[2] https://hyo-ue4study.tistory.com/346

 

[CPP]반환 값 최적화(return value optimization)RVO란?/NRVO

hyo-ue4study.tistory.com/345 [CPP-effective] 4-4 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려 하지말자. 이전에 작성한 글을 쓰고, 기존의 코드에 멀쩡하게 들어 있는 '값에 의한 전달' 부분을 '

hyo-ue4study.tistory.com

[3] http://ajwmain.iptime.org/programming/book_summary/%5B03%5Deffective_modern_cpp/effective_modern_cpp.html#I25

 

Effective Modern C++ 정리

컴파일러가 참조 축약(collapse) 문맥에서 참조에 대한 참조를 만들어 내면, 그 결과는 하나의 참조가 된다. 원래의 두 참조 중 하나라도 좌측값 참조이면 결과는 좌측값 참조이고, 그렇지 않으면

ajwmain.iptime.org

[4] https://devwoodo.blogspot.com/2016/06/copy-elision-rvo-nrvo.html

 

copy elision ,RVO, NRVO

복사 및 이동 생성자를 zero-copy pass-by-value (무복사 값에 의한 전달)로 최적화 하는 것. 특정 상황(아래 서술)에서 객체의 복사 및 이동 생성자를 생략함. 함수의 return 문과 관련되는 경우 이를 RVO(r

devwoodo.blogspot.com