결과는 컴파일러나 모드에 따라 다를 수 있지만 전체 문맥적으로 본다면 이해하는데 큰 무리는 없을겁니다
C/C++를 써야만 하는 혹은 쓸 수 밖에 없는 상황들이라는 것은 대부분 성능,속도, 자원 사용의 최적화 등이지요. Visual Studio 2010에 새로이 추가된 Rvalue concept 이나 사용 예들도 정확히 이러한 상황들에 대응하기 위한 것이라 할 수 있을 겁니다.
Rvalue의 경우에도 class의 개발 시 move constructor/operator 를 추가하여, method를 호출할 때, 매개변수를 통하여 객체를 전달하는 과정에서 임시객체의 생성/소멸을 가능한 막아보자는 것이지요. 하지만 이러한 최적화 과정은 사실 Rvalue concept의 도입이전에도 다양한 형태로 시도되고, 실제 컴파일러에 적용되었습니다. 오늘 설명 드리고자 하는 (N)RVO 도 Rvalue와 비슷한 맥락의 최적화 기법 중 하나입니다.
간단한 예제를 통해서 그 내용을 살펴 볼까 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <stdio.h> class RVO { public: RVO() { printf("I am in constructor\n"); } RVO (const RVO& c_RVO) { printf ("I am in copy constructor\n"); } ~RVO() { printf ("I am in destructor\n"); } int mem_var; }; RVO MyMethod (int i) { RVO rvo; rvo.mem_var = i; return (rvo); } int _tmain(void) { RVO rvo; rvo = MyMethod(5); return 0; } |
위 예제를 머리 속으로 컴파일 해서 수행해 보시고 출력 결과가 어떠할지, 그리고 생성자, 복사 생성자, 파괴자가 각각 몇 번이나 호출될지 정확히 예측하실 수 있으실까요? 위 source를 어떠한 optimiation도 켜지 않은 상태로, debug mode에서 수행하면 다음과 같은 결과를 출력합니다.
I am in constructor <-- _tmain의 RVO rvo; 행에 의해서 호출될 겁니다.
I am in constructor <-- MyMethod 함수의 RVO rvo; 행에 의해서 호출될 겁니다.
I am in copy constructor <-- 이 녀석은 어디서 호출되는 걸까요? MeThod 함수의 return (rvo); 에서 호출됩니다.
I am in destructor
I am in destructor
I am in destructor
생성자, 복사 생성자, 파괴자는 총 6회 호출 되었음을 알 수 있죠. 어디서 생성자, 복사 생성자, 파괴자가 생성되었는지를 좀 더 명확하게 살펴보려면 위 코드를 컴파일 한 후 assembly code를 드려다 보면 좋습니다.
먼저 _tmain() 부터 확인해보죠.
RVO rvo;
00F41518 lea ecx,[ebp-10h] // rvo의 위치는 [ebp-10h] 입니다.
00F4151B call RVO::RVO (0F41140h) // 생성자가 호출되었군요 1
00F41520 mov dword ptr [ebp-4],0
rvo = MyMethod(5);
00F41527 push 5
00F41529 lea eax,[ebp-58h] // 5는 [ebp-58h]에 저장됩니다.
00F4152C push eax // stack에 push하구요.
00F4152D call MyMethod (0F410DCh) // 함수를 호출했습니다.
00F41532 add esp,8 // stack을 정리 했구요.
00F41535 mov dword ptr [ebp-5Ch],eax // 반환 값을 [ebp-5ch]로 담고는
00F41538 mov ecx,dword ptr [ebp-5Ch] // 그 값을 다시 ecx로 옮깁니다.
00F4153B mov edx,dword ptr [ecx] // ecx가 가리키는 메모리의 값을 edx로 옮기고
00F4153D mov dword ptr [ebp-10h],edx // rvo 변수가 가리킬 수 있도록 변경합니다.
00F41540 lea ecx,[ebp-58h] // 이건 반환되었던 객체죠?
00F41543 call RVO::~RVO (0F4101Eh) // 반환 되었던 객체의 파괴자를 호출합니다. 5
return 0;
00F41548 mov dword ptr [ebp-54h],0
00F4154F mov dword ptr [ebp-4],0FFFFFFFFh
00F41556 lea ecx,[ebp-10h] // rvo 입니다.
00F41559 call RVO::~RVO (0F4101Eh) // rvo에 대한 파괴자를 호출하는군요. 6
00F4155E mov eax,dword ptr [ebp-54h]
이젠 MyMethod도 살펴 보겠습니다.
RVO rvo;
00F413EF lea ecx,[ebp-10h] // rvo의 위치는 [ebp-10h] 입니다.
00F413F2 call RVO::RVO (0F41140h) // 생성자가 호출되었군요. 2
00F413F7 mov dword ptr [ebp-4],1
rvo.mem_var = i;
00F413FE mov eax,dword ptr [ebp+0Ch]
00F41401 mov dword ptr [ebp-10h],eax
return (rvo);
00F41404 lea eax,[ebp-10h]
00F41407 push eax
00F41408 mov ecx,dword ptr [ebp+8] // [ebp+8] 위치의 임시 객체에 대해서
00F4140B call RVO::RVO (0F41145h) // 복사 생성자를 호출하는군요. 3
00F41410 mov ecx,dword ptr [ebp-54h]
00F41413 or ecx,1
00F41416 mov dword ptr [ebp-54h],ecx
00F41419 mov byte ptr [ebp-4],0
00F4141D lea ecx,[ebp-10h] // [ebp-10h] 위치의 객체는 rvo 입니다.
00F41420 call RVO::~RVO (0F4101Eh) // 파괴자를 호출하는군요 4
00F41425 mov eax,dword ptr [ebp+8]
}
(복사)생성자, 파괴자의 호출 code는 붉은색으로 표시하였고, 옆에 호출 순서에 따라 번호를 써 두었습니다. 근데 여기서 우리가 유심히 살펴보았음 직한 녀석은 3번의 복사 생성자 호출과 5번의 파괴자 호출입니다. 조금만 고민해 보면 MyMethod()에서 객체를 반환하기 위해서 임시 객체를 생성하고, 생성된 임시 객체를 반환한 후, 생성된 임시 객체의 파괴자를 호출하는 일련의 과정은 없어도 될 것 같지 않으세요? 이미 _tmain에서 생성된 객체가 있으므로, 임시 객체의 생성/파괴는 사실 불필요 하죠.
임시 객체를 생성해야 했던 이유는 함수의 반환 값으로 객체를 전달하기 위해서만 쓰였잖아요. 이제 제가 말씀 드리고 싶어한 걸 설명할 시간이 되었네요.
RVO는 Return Value Optimization 이라는 기법인데요. 이것이 뭔고 하니 위 예와 같이 어떤 함수가 객체를 반환해야 할 경우에 필요하지 않은 임시 객체를 생성하지 않도록 최적화 하는걸 말합니다. 그런데 RVO는 MyMethod() 에서 처럼 반환 객체가 변수명을 가지는 경우는 최적화가 되질 않았어요. 그래서 몇몇 사람들이 “변수가 이름을 가지는 경우에도 최적화의 대상에 포함시키자”로 주장했고, 이를 NRVO, Named Return Value Optimization 이라고 구분하여 불렀습니다. 그리하여 ISO/ANSI C++ 위원회에서 1996년에 이 이 둘 모두에 대해서 최적화 될 수 있음을 발표했다고 하는군요.(사실 발표하는 것이 뭐 어렵습니까? compiler 개발사만 어렵지..) 여하둥둥 그리하여 Visual Studio 2005에서 NRVO에 대한 최적화 기능이 포함되었습니다.
Visual Studio에서 NRVO를 가능하게 하기 위해서는 /O2 compiler option을 주면 됩니다.
(프로젝트 설정에서 Optimization : Maximize Speed를 선택하시면 됩니다.)
vs2017 에서 Release 모드인경우 O2가 default
Debug mode 에서는 '사용 않함 /Od' 가 기본 모드
이제 최적화의 결과물을 살펴 보시죠.
I am in constructor
I am in constructor
I am in destructor
I am in destructor
위의 출력결과와 사뭇 다른 것은 복사생성자의 호출이 빠졌고, 파괴자의 호출이 하나 줄어들었음을 알 수 있습니다.
(어느 부분이 생략되었는지 예측하실 수 있겠죠?)
내용을 명확하게 하기 위해서 컴파일된 결과물을 disassmebly 한 결과
MyMethod() 입니다.
RVO MyMethod (int i)
{
RVO rvo;
01351030 push esi
01351031 call RVO::RVO (1351000h) // rvo 객체 생성자 호출이 여기 있군요.
rvo.mem_var = i;
01351036 mov dword ptr [esi],5 // 여기도 중요하죠. esi를 통해서 전달된 객체에다가 5를 넣어버립니다. 객체생성이 없죠.
return (rvo);
0135103C mov eax,esi
}
0135103E ret
_tmain() 입니다.
RVO rvo;
01351045 lea eax,[rvo]
01351048 push eax
01351049 call RVO::RVO (1351000h) // rvo 객체 생성자 호출이지요
rvo = MyMethod(5);
0135104E lea esi,[rvo] // 여기가 핵심입니다. 함수의 호출 결과를 rvo로 할당하는게 아니라,
rvo 값을 esi를 통해서 MyMethod로 넘겨버립니다.
01351051 call MyMethod (1351030h)
01351056 call RVO::~RVO (1351020h) // 파괴자 호출
return 0;
0135105B call RVO::~RVO (1351020h) // 파괴자 호출
01351060 xor eax,eax
01351062 pop esi
}
01351063 mov esp,ebp
01351065 pop ebp
01351066 ret
여기저기 최적화 루틴 때문에, 앞서의 코드와는 많이 달라졌지만, 어떤 식으로 최적화가 진행되었는지를 미루어 짐작해 볼 수 있습니다.
MEC++ 20항목에 나오는 예제를 그대로 활용해보자.
- class Rational
- {
- public:
- Rational(int numerator = 0, int denominator = 1);
- ...
- int numerator() const;
- int denominator() const;
- };
- const Rational operator * (const Rational &lhs, const Rational &rhs);
- // 1.
- const Rational* operator * (const Rational &lhs, const Rational &rhs);
- // 2.
- const Rational& operator * (const Rational &lhs, const Rational &rhs);
- inline const Rational operator * (const Rational &lhs, const Rational &rhs)
- {
- return Rational(lhs.numerator * rhs.numerator,
- lhs.denominator * rhs.denominator);
- }
- template<typename T>
- opeator + (const T& lhs, const T& rhs)
- {
- return T(lhs) += rhs;
- }
- class RVOSample
- {
- public:
- RVOSample();
- ~RVOSample();
- int value;
- };
- RVOSample TestNRVO(int num)
- {
- RVOSample rvo;
- rvo.value = num;
- return rvo; // 임시 객체가 생성되지 않는다.
- }
- RVOSample TestNRVO(int num)
- {
- RVOSample rvo;
- rvo.value = num;
- /- 조건에 의해 반환값이 다른 경우 NRVO로 최적화되지 않는다. *-
- if (5 == num)
- {
- rvo.value *= 2;
- return rvo;
- }
- return rvo;
- }
- #include "stdafx.h"
- #include <iostream>
- class RVOTest
- {
- public:
- RVOTest() { std::cout << "Constructor\n"; }
- RVOTest(const RVOTest& rhs) { std::cout << "Copy Constructor\n"; }
- RVOTest(RVOTest&& rhs) { std::cout << "Move Constructor\n"; }
- ~RVOTest() { std::cout << "Destructor\n"; }
- };
- RVOTest RVOFunc()
- {
- return RVOTest();
- }
- RVOTest NRVOFunc()
- {
- RVOTest r;
- return r;
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- /*
- Constructor
- Destructor
- */
- {
- RVOTest r1 = RVOFunc();
- }
- std::cout << "=============================\n";
- //이건 최적화 모드에 따라 달라짐 Debug , Release 모드 기본 최적화 옵션이 다른데 각각 모드를 변경해보면 차이를 알 수 있음
- /*
- Constructor
- Destructor
- */
- {
- RVOTest r2 = NRVOFunc();
- }
- return 0;
- }
ref : http://egloos.zum.com/himskim/v/3630181
ref : http://egloos.zum.com/sweeper/v/1942099
'프로그래밍(Programming) > c++, 11, 14 , 17, 20' 카테고리의 다른 글
VS2017 C++ 17 활성화 옵션 (1) | 2018.05.14 |
---|---|
Rvalue References: C++0x Features in VC10, Part 2 (0) | 2018.04.29 |
메모리관점에서 본 전역(global)변수와 정적(static)변수의 차이 (0) | 2018.02.16 |
레지스터와 연산 처리과정 EAX, EDX, ECX, EBX, ESI, EDI, ESI, EDI (0) | 2018.02.13 |
__declspec(align(n)) 으로 바이트 정렬하기 (0) | 2018.02.10 |