관리 메뉴

기억을 위한 기록들

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

C & CPP

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

에드윈H 2021. 4. 14. 19:59

hyo-ue4study.tistory.com/345

 

[CPP-effective] 4-4 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려 하지말자.

이전에 작성한 글을 쓰고, 기존의 코드에 멀쩡하게 들어 있는 '값에 의한 전달' 부분을 '참조에 의한 전달'로 무작정 바꾸려고 하면 안 된다. 실제로 있지도 않은 객체의 참조자를 넘길 수도 있기

hyo-ue4study.tistory.com

이펙티브 c++를 작성하다가 등장하여 관련하여 찾아보았다.

 

* 틀린 부분이 있을 수도 있습니다.

 

예제 클래스로 살펴보면 int num 멤버변수를 갖고 있는 클래스 T가 있다.

class T {
public:
    /*생성자*/
	T(int a, string n) 
		:num(a)
		, name(n)
	{
		cout <<name<< "객체 생성자 호출 " << endl;
	} 
    
    
    /*복사 생성자*/
	T(const T& ref)  
		: num(ref.num)
		, name("empty")
	{
		cout << ref.name << "객체를 복사한, ";
		cout << name << " 새로운 객체 복사생성자 호출 " << endl;
	}
    
    /*소멸자*/
	~T()
	{
		cout <<name << "객체 : 소멸자 호출" << endl;
	}
    
    
	int num;

	string name;
};

별 다른 기능이 없긴 하다. 

 

여기서 전역 함수로 해당 클래스를 복사해 값으로 반환해주는 함수가 있다.

const T copyT(const T& newT)
{
	T b(newT.num,"b");  //파라미터로 받은 T객체의 이름을 b라고 넣음.
	return b;
}

한 객체를 복사하기 위해 위 함수를 실행해보면

int main()
{
	T a(5,"a");
	copyT(a);
	return 0;
}

실행 결과로 2번의 클래스 생성과 1번의 복사와 3번의 파괴가 일어난다.

 

 

 

순서대로 확인해보면 

1. a객체 생성자 호출 : main 함수 a객체 생성한다

2. b객체 생성자 호출 : copyT 함수 안에서 b라는 이름을 가진 T 객체 생성한다.

3. b이름 객체를 복사한, empty 이름을 가지는 복사생성자 호출 :
     return 되는 b객체를 복사한 empty이름 객체  복사생성자 호출

4. b객체 소멸자 호출 : copyT함수가 끝나며 스택 메모리를 벗어나며 소멸이 바로 진행되는 b이름 객체 소멸

5. empty객체 소멸자 호출 :  main 함수로 돌아와 main함수 종료되며 emtpy이름  객체 소멸

6. a객체 소멸자 호출  : main 함수로 돌아와 main함수 종료되며 a이름 객체 소멸

 

난 단지 a객체의 정수값을 가지는 객체를 하나 더 만들어서 return 받으려고 한 것뿐인데, 

 

불필요한 생성과 소멸이 더 일어난다. 만약에 T 클래스 자체의 크기가 커지면 더욱 더 비효율적이게 된다.

 

여기서 이제 RVO(return value optimization)이 적용되는 새로운 객체를 반환되게 하는 복사 함수로 만들어준다면, 출력이 달라진다.

 

main 함수에서 copyT_rvo 함수를 호출해주면 

int main()
{
	T a(5,"a");
	copyT_rvo(a);
	return 0;
}

 

copyT_rvo함수에서는 객체 생성 후 리턴 해주는것이 아니라 new라는 이름을 가진 객체를 바로 리턴해준다.

const T copyT_rvo(const T& newT)
{
	return T(newT.num, "new");
}

 

 

 

이전과 다르게, 복사생성자가 호출되지 않고 단 2개의 객체의 생성과 소멸만 일어난다.

 

 

얼핏보기엔 짧은 코드의 차이지만, 내부적으로는 많은 차이가 일어나는 것을 확인 할 수 있다.

 

그런데 재밌는 부분은 해당 코드는 debug모드에서 실행시켰을때의 예시이고, Release모드로 실행시켜보면 다르게 작동된다.

 

const T copyT(const T& newT)
{
	T b(newT.num,"b");  //파라미터로 받은 T객체의 이름을 b라고 넣음.
	return b;
}

이전의 copyT함수를 실행시키면 결과는 불필요한 복사생성자를 호출 하지 않는다. 

 

차이는 바로 Release모드에서는 컴파일러가 이를 감지하고, 최적화를 해주기 때문이다. copyT 함수에서 생성된 T클래스 객체가 return 을 바로 해주는 것을 인지하고 최적화 해주게 되는 것이다.

 

RVO는 컴파일러 상관없이 해주기에 어떤 모드에서든지 최적화를 해준다.

 

 

그리고 NRVO라고 불리는 것도 있는데 Named Return Value Optimization이라고 부르는것이 있는데 이게 바로 

const T copyT(const T& newT)
{
	T b(newT.num,"b");  //파라미터로 받은 T객체의 이름을 b라고 넣음.
	return b;
}

copyT 함수처럼 객체에 이름이 붙여진 반환의 차이라고 생각하면 될것같다.  

 

정리하자면 RVO는 

const T copyT_rvo(const T& newT)
{
	return T(newT.num, "new");
}

 이 자체로 임시 객체 생성없이 생성하는 객체는 바로 반환해주는 것이고, NRVO는 이름 붙여진 객체 반환 즉, 이전에 비교한 copyT함수가 예다. 

const T copyT(const T& newT)
{
	T b(newT.num,"b");  //파라미터로 받은 T객체의 이름을 b라고 넣음.
	return b;
}

비교를 위해 보여준 함수로, NRVO는 Debug모드에서는 최적화가 안되었지만, Relase모드에서는 최적화가 된것을 확인했다. 이는 차이로 RVO는 Relase모드 Debug 모드 상관없이 반환 최적화를 해주는 반면, 

NRVO는 최적화 옵션 /O1(크기 최소화)부터 동작을 하게 되서 Debug모드에서는 작동되지 않는다.