NRVO

Named Return Value Optimazation은 어떻게 작동하는걸까?

C++ 코딩 표준

내가 따르는 코딩 표준을 다음의 규칙이있다.

지역 객체를 반환할 때 NRVO의 이점을 활용한다. 이는 함수 내에 하나의 return문 만 쓴다는 것을 의미하며, 이것은 값으로 객체를 반환할 때만 적용된다.

여기서 RVO, NRVO 라는것을 처음알게되었다.

Copy elision

그래서 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

Untitled

release x64

Untitled

retnrvo1, 2에서는 최적화를 해주고 변수 두개를 각각 반환하는 notnrvo에서는 최적화를 해주지 못하고있다.

여기서 재밌는건 retnrvo2에서 최적화가 되었는데, 컴파일러가 똑똑해지면서 return하는 변수를 하나로 통일만한다면 nrvo 최적화를 해주는 듯 하다.

그러니 꼭 return문을 하나만 쓸 필요는 없는게 됩니다.

어셈을 까보면

함수 인자 i를 edx에 넘기고 객체의 주소를 rcx에 넘겨서 함수콜을 하고 이 변수 c가

아예 함수 내부의 변수를 완전히 대체해서 사용한다.

Untitled

Untitled

중간에 코드가 들어가있는건, 가운데 코드가 없어서 최적화를 수행하나 싶어서 적당히 끼워넣었다.

if문 끝나고 ret을 호출하는게아니라 함수 맨끝으로 jump로 return 이후로 보내 call 호출을 한다.

Untitled

그래서??

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에서도 해준다.

이것도 그만알아보자.

Posted 2022-12-21