c++ 컴파일러는 빠른 속도를 제공해주기 위해서 다양한 최적화를 제공합니다.
그중에 이번 글에서는 반환값 최적화에 대해서 알아보며 내부 동작을 이해하는 시간을 가져 봅니다.
#include <iostream>
#include <string>
class CTest
{
public:
CTest()
{
std::cout << "CTest Constructor, value : " << m_str.c_str() << std::endl;
}
CTest(const std::string & str) : m_str(str)
{
std::cout << "CTest Constructor strParam, value : " << m_str.c_str() << std::endl;
}
CTest(const CTest & t)
{
this->m_str = t.m_str;
std::cout << "CTest Copy Constructor, value : " << m_str.c_str() << std::endl;
}
CTest(CTest&& t) noexcept
{
this->m_str = std::move(t.m_str);
std::cout << "CTest Move Constructor, value : " << m_str.c_str() << std::endl;
}
~CTest()
{
std::cout << "CTest Destructor, value : " << m_str.c_str() << std::endl;
}
public:
std::string m_str;
};
class CTestRapper
{
public:
CTestRapper(const CTest& ct)
{
std::cout << "CTestRapper Constructor" << std::endl;
}
~CTestRapper()
{
std::cout << "CTestRapper Destructor" << std::endl;
}
};
// RVO(Return Value Optimization) 예제
CTest CallRVO()
{
std::cout << "CALL CallRVO Function" << std::endl;
return CTest("RVO");
}
// NRVO(Named Return Value Optimization) 예제
CTest CallNRVO()
{
std::cout << "CALL CallNRVO Function" << std::endl;
CTest tempCTest("NRVO");
return tempCTest;
}
RVO 동작 방식을 알아 보기 위해서 위와 같은 예제 코드를 준비 하였습니다.
함수의 반환 되는 임시 객체에 std::move 적용
반환값 최적화를 공부하기 전에 Move Sementics를 처음 배우게 되었을 때 시절의 이야기를 해보려고 합니다.
다들 그 당시 다음과 같은 상상을 해본적이 있을 것 같습니다.
"함수 안에서 생성되는 임시 객체에 대한 리턴시 복사 생성이 되고 있으니 해당 로직을 이동 연산을 적용하자."
CTest && CallReturnMove()
{
std::cout << "CALL CallReturnMove Function" << std::endl;
CTest tempCTest("NRVO");
return std::move(tempCTest);
}
int main()
{
CTest ct = CallReturnMove();
}
위의 로직을 실행 시켜보면 우리가 예상한대로 호출이 되며
CTest의 m_str 객체가 이동연산이 되면 복사 연산보다 더욱 효율적으로 동작하는 것을 확인 할 수 있습니다.
그러나 아쉽게도 해당 로직이 내부적으로 컴파일러에게 잘못된 내용을 전달하여 최적화 수행을 방해하게 되며
"warning C4172: 지역 변수 또는 임시: tempCTest의 주소를 반환하고 있습니다." 같은 warning을 호출 하게 됩니다.
RVO 동작 호출
RVO란 return value optimization으로 CallRVO 형태 처럼 return 값을 rvalue로 호출 할 때 동작하는 최적화 기법을 말합니다.
// RVO(Return Value Optimization) 예제
CTest CallRVO()
{
std::cout << "CALL CallRVO Function" << std::endl;
return CTest("RVO");
}
/// 생략...
int main()
{
CTest Ct1 = CallRVO();
}
위의 코드를 실행 시키면 "CTest의 생성자 및 소멸자는 몇 번 호출되어야 할까요?"
코드로 파악해보면 다음과 같은 결과로 출력되어야 할 것으로 예상됩니다.
- CallRVO에서 CTest의 생성자 호출, 소멸자 호출(Ct1의 복사 생성 완료 후)
- main 함수에서는 CTest의 복사 생성자 호출 및 소멸자 호출
하지만 실제로 실행해보면 다음과 같은 결과를 얻는 것을 확인 할 수 있습니다.
복사 생성자가 호출 되지 않고 있으며 소멸자도 1회만 호출 되고 있습니다. 놀라운 결과 입니다.
실제 디버깅으로 확인 해보면 다음의 위치에 생성자 및 소멸자가 호출 되는 것을 확인 할 수 있습니다.
- CallRVO 함수의 "return CTest("RVO");" 구문에서 CTest의 생성자 호출
- Main함수의 중괄호가 끝나는 지점에서 CTest의 소멸자 호출
CallRVO 함수에서 Main함수의 "CTest Ct1" 객체 메모리 주소에 객체를 생성하는 방식으로 최적화가 동작하고 있는 것으로 보입니다.
컴파일러는 매우 똑똑하게 객체의 생성을 최소화 하는 형식으로 최적화 되고 있네요. 든든합니다.
리턴 값이 일치하지 않은 호출
위의 내용을 자세하게 알아보기 위해서 Main에서 CTestRapper를 통해서 CallRVO함수를 결과를 저장하도록 실행해보면 명확하게 알 수 있습니다.
int main()
{
CTestRapper CtRapper1 = CallRVO();
}
CTestRapper를 사용하면 함수의 리턴 값과 복사생성되는 객체의 타입이 일치 하지 않아 최적화 로직이 실행 되지 않습니다.
코드상으로 보이는 대로 호출 되는 것을 확인 할 수 있습니다. (컴파일러의 최적화 기준에 도달하지 못한 것 같네요..)
- CallRVO에서 CTest의 생성자 호출, 소멸자 호출(CtRapper1 생성자 호출 완료 후)
- main 함수에서는 CTestRapper의 생성자 호출 및 소멸자 호출
NRVO 동작 호출
NRVO란 named return value optimization으로 CallNRVO 형태 처럼 return 값을 rvalue로 호출 할 때 동작하는 최적화 기법을 말합니다.
RVO와는 다르게 이름을 가진 객체를 리턴하도록 구현하고 있습니다.
// 생략...
// NRVO(Named Return Value Optimization) 예제
CTest CallNRVO()
{
std::cout << "CALL CallNRVO Function" << std::endl;
CTest tempCTest("NRVO");
return tempCTest;
}
int main()
{
CTest ct2 = CallNRVO();
}
코드로 파악해보면 다음과 같은 결과로 출력되어야 할 것으로 예상됩니다.
- CallNRVO에서 CTest의 생성자 호출, 소멸자 호출(Ct1의 복사 생성 완료 후)
- main 함수에서는 CTest의 복사 생성자 호출 및 소멸자 호출
하지만 실제로 실행해보면 다음과 같은 결과를 얻는 것을 확인 할 수 있습니다.
디버그 환경
- 디버그 환경에서는 반환값을 rvalue처럼 판단하여 move 연산을 수행해서 최적화 합니다.
릴리즈 환경
- 릴리즈 환경에서는 RVO와 같이 동작하는 것을 볼 수 있습니다.
RVO 호출과 같이 컴파일러가 매우 똑똑하게 NRVO를 최적화 해주고 있습니다.
축하합니다. 앞으로 우리는 이러한 구문을 보았을 때 똑똑한 컴파일러에게 감사함을 느끼며
최적화를 믿고 맏길 수 있게 되었습니다.
긴 글을 읽어 주셔서 감사합니다. ( _ _ )
'c++ > c++' 카테고리의 다른 글
[c++] find_if (0) | 2024.09.14 |
---|---|
[c++ 예제] 멀티스레드에 안전한 notify_queue 클래스 (0) | 2020.07.10 |
[c++] 키워드 (0) | 2020.03.08 |
[c++] vector (0) | 2019.10.03 |
[c++] chrono를 사용한 수행 시간 출력 클래스 (0) | 2019.09.20 |