NRVO
Named Return Value Optimazation은 어떻게 작동하는걸까?
내가 따르는 코딩 표준을 다음의 규칙이있다.
지역 객체를 반환할 때 NRVO의 이점을 활용한다. 이는 함수 내에 하나의 return문 만 쓴다는 것을 의미하며, 이것은 값으로 객체를 반환할 때만 적용된다.
여기서 RVO, NRVO 라는것을 처음알게되었다.
그래서 NRVO가 어떻게 최적화를 하는지 뒤져보는 포스팅이다.
찾아보면 cpp 표준에도 언급되어있는 최적화이다.
msvc에서 -O1부터 작용한다.
inline 최적화 옵션을 빼고 테스트를 진행했다.
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <iostream>
using namespace std;
class a
{
public:
a(const a& a)
{
cout << "call copy constructor" << endl;
}
a(int p)
{
aa = p;
cout << "call constructor" << endl;
}
private:
int aa;
long long b;
int64_t k;
short q;
char nn;
};
a retnrvo(int k)
{
a bb(1);
printf("bb 0x%p\n", &bb);
if (k > 1)
{
bb = a(k);
}
else
{
bb = a(k + 13);
}
return bb;
}
a retnrvo2(int k)
{
a aa(1);
printf("aa 0x%p\n", &aa);
if (k > 1)
{
aa = a(k + 13);
return aa;
}
else
{
aa = a(k + 3);
return aa;
}
}
a notnrvo(int k)
{
a b = a(k);
a aa = a(2 * k);
if (k > 1)
{
printf("b 0x%p\n", &b);
return b;
}
else
{
printf("aa 0x%p\n", &aa);
return aa;
}
}
int main()
{
int i;
cin >> i;
cout << "retnrvo call" << endl;
a c = retnrvo(i);
cout << "retnrvo2 call" << endl;
a d = retnrvo2(i);
cout << "notnrvo call" << endl;
a e = notnrvo(i);
printf(" c : 0x%p\n", &c);
printf(" d : 0x%p\n", &d);
printf(" e : 0x%p\n", &e);
return 0;
}
debug x64
release x64
retnrvo1, 2에서는 최적화를 해주고 변수 두개를 각각 반환하는 notnrvo에서는 최적화를 해주지 못하고있다.
여기서 재밌는건 retnrvo2에서 최적화가 되었는데, 컴파일러가 똑똑해지면서 return하는 변수를 하나로 통일만한다면 nrvo 최적화를 해주는 듯 하다.
그러니 꼭 return문을 하나만 쓸 필요는 없는게 됩니다.
어셈을 까보면
함수 인자 i를 edx에 넘기고 객체의 주소를 rcx에 넘겨서 함수콜을 하고 이 변수 c가
아예 함수 내부의 변수를 완전히 대체해서 사용한다.
중간에 코드가 들어가있는건, 가운데 코드가 없어서 최적화를 수행하나 싶어서 적당히 끼워넣었다.
if문 끝나고 ret을 호출하는게아니라 함수 맨끝으로 jump로 return 이후로 보내 call 호출을 한다.
그래서??
c++를 공부하다보면 l value r value에 대해서 알게될거다. 이정도까지는 알아놓는게 좋고 근데 이게 modern으로 넘어가면서 더 복잡해졌는데 거기에 대한 스펙은 cppreference에 있다
그리고 move constructor move assignment 에대해서 알게되는데 오 이런게 있군하면서 쓰려고보면 exception까지 고려해야하고 온갖 이유 모를 컴파일에러에 정신이 혼미해지고 통째로 주석을 친 기억이있다.
그리고 한발 더 나아가서 cpp 공부하는 사람들에게 잘아는 mordern effective cpp에서 나와 이게 어떻게 레퍼런스에 언급이 되는지 이해가 안가는(정식 Cpp의 용어는 아니다.) perfect forwarding, universal reference 그리고 그 지원 함수들인 std::forward, std::move 이걸 다 학습하려고보면 이런거까지알아야하는데 cpp를 접을까 고민이 될정도다.
근데 nrvo의 핵심을 안다면, 컨셉만 이해하고 윗 부분은 그냥 넘어가도 무방하다. 위에 언급한 내용은 저도 잘모름 일단 레퍼런스라 링크단거지 안 읽어봤고 읽을 생각없음.
그만 알아보자.
update 23/02/03
추가 내용.
오늘자로 테스트 해볼 일이 있어서 한번 테스트 해봤었는데 재미있는 결과가 나왔다.
- msvc 142(VS2019)에서 디버그 모드에서는 최적화 안 해주고 release에서 -O1에서도 최적화를 해준다. 최적화 옵션이 없어도 해준다.
- msvc 143(VS2022)에서 디버그 모드에서도 최적화를 해준다.
msvc 문서에선 일단 O2를 기준으로 얘기하고 있다. 위에있는 스탠다드의 링크를 봐도 O2를 기준으로 얘기하고있다.
https://learn.microsoft.com/en-us/cpp/build/reference/zc-nrvo?view=msvc-170
최신 msvc에서 이 최적화에 대한 스펙은 이 글이 잘 설명되어있다
https://devblogs.microsoft.com/cppblog/improving-copy-and-move-elision/
해당 링크에서보면 -O2에서부터 해준다고 명시되어있다. 왜 온갖 글에서 O1에 해준다고 되어있는진 나도 잘모르겠다.
왜 최적화가 되는지는 아마 다른 옵션이 이 최적화 기능을 켜는거같은데 정확한 옵션을 봐도 잘 모르겠다.
저 글을 쓸 당시 아마 /O1로 테스트를 했었다. 현재 노트북에서도 VS22이지만 디버그에선 최적화를 해주지 않고 정확히 O1 optimize에서부터 최적화를 수행 해줬었다.
1
/permissive- /ifcOutput "x64\Release\" /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /Od /Ob0 /sdl /Fd"x64\Release\vc143.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Fp"x64\Release\TEST.pch" /diagnostics:column
여기에서도 /Zc:nrvo 옵션을 붙이면 작동은 잘한다.
1
/permissive- /ifcOutput "x64\Release\" /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O1 /Ob0 /sdl /Fd"x64\Release\vc143.pdb" /Zc:inline /fp:precise /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /Gd /Oi /MD /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Fp"x64\Release\TEST.pch" /diagnostics:column
처음 테스트했던 머신이 VS22를 최근에 깔았어서 혹시 업데이트 문제인가 싶어 VS22 업데이트 후 새 프로젝트 생성했더니 debug에서도 최적화가 됨
해당 옵션
1
/JMC /permissive- /ifcOutput "x64\Debug\" /GS /W3 /Zc:wchar_t /ZI /Gm- /Od /sdl /Fd"x64\Debug\vc143.pdb" /Zc:inline /fp:precise /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /RTC1 /Gd /MDd /FC /Fa"x64\Debug\" /EHsc /nologo /Fo"x64\Debug\" /Fp"x64\Debug\Project1.pch" /diagnostics:column
https://stackoverflow.com/questions/74422765/nrvo-bug-in-debugger-in-msvc-17-4
아마 저 업데이트 이후로 디버그 모드에서도 최적화를 해주는 듯하다.
https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/overview/what-s-new-for-visual-cpp-in-visual-studio.md
버그가아니라 정식기능이라고한다. 흠;
이게머선;;
아마 O1에서도 해주는데 문서화에 O2로만 기술을 해놓은거 같다.
정리하면 O1에서도 해준다.
이것도 그만알아보자.