(번역) 실시간 컴퓨터 비전을 위한 C++ 최적화 (0) - Const reference를 쓰세요!

Davide Faconti의 CPP Optimization Diary 블로그 글 중 “Value Semantics vs references”을 적당히 번역했습니다. 원글 링크는 여기를 봐주세요.


Pass by Value


이번 글의 내용은 사실 어느정도 C++ 개발을 해보신 분들은 이미 잘 아는 내용일 것이라고 믿습니다.

그래도 저는 종종 이런 코드를 짜는 사람들을 봅니다.

1
2
3
4
5
6
7
8
bool OpenFile(str::string filename);

void DrawPath(std::vector<Points> path);

Pose DetectFace(Image image);

Matrix3D Rotate(Matrix3D mat, AxisAngle axis_angle);

위의 함수들은 제가 방금 만들어낸 것이긴 하지만, 실제로 제품으로 배포되는 코드에서도 종종 보이는 코드들입니다.

위 코드들은 모두 함수 인자를 passing by value 형태로 넘깁니다.

즉, 이 함수를 호출할 때 마다 함수 인자들은 우선 복사가 되고, 그 후에 함수로 넘겨집니다.

복사 작업은 종종 굉장히 무거운 작업이 될 수 있습니다. 작은 객체를 복사할 때는 그리 무겁지 않지만, 엄청 큰 객체를 복사하면 무거울 수 있죠. 동적 heap memory allocation이 들어간다면 더 무거울 수도 있겠습니다.

위의 예제들 중 아마 무시할 수 있을 정도의 굉장히 작은 양의 overhead가 나타나는 객체도 있습니다. Matrix3DAngleAxis 같은 객체는 heap allocation이 없이도 복사가 됩니다.

하지만 overhead가 작아도, 굳이 CPU 사이클을 이런 의미없는 복사에 낭비할 필요가 있을까요? 복사를 안하면 되지 않나요?

아래 예제를 한번 봅시다.

1
2
3
4
5
6
7
8
bool OpenFile(const str::string& filename); // string_view is even better

void DrawPath(const std::vector<Points>& path);

Pose DetectFace(const Image& image);

Matrix3D Rotate(const Matrix3D& mat, const AxisAngle& axis_angle);

이번 예제에서 우리는 reference semantic이라는 기법을 사용했습니다.

이 방법 대신에 C-style의 포인터를 사용해서 같은 결과를 얻을 수도 있겠습니다. 다만 포인터와 비교했을 때 reference semantic 기법이 다른 점이라면, reference semantic은 컴파일러에 다음과 같은 의미를 전달합니다.

  • 함수 인자들이 Constant 하다는 점. 함수를 호출한 쪽에서도, 함수 내부에서도 이 인자 값을 수정하지 않겠다는 점을 명시합니다.
  • 함수 인자들이 Reference라는 점. 이미 값이 존재하는 인자들을 ‘refer’ 하는 것입니다. C-style 포인터는 nullptr 가 값이 될 수 있지만, reference인 경우는 안되겠네요.
  • 포인터를 사용하는게 아니니, 우리는 이 객체의 ownership을 넘겨주는게 아닙니다.

어쨋든 이 방법을 사용하면 아래에서 보는 것 처럼 계산량이 엄청나게 줄어듭니다.

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
size_t GetSpaces_Value(std::string str)
{
size_t spaces = 0;
for(const char c: str){
if( c == ' ') spaces++;
}
return spaces;
}

size_t GetSpaces_Ref(const std::string& str)
{
size_t spaces = 0;
for(const char c: str){
if( c == ' ') spaces++;
}
return spaces;
}

const std::string LONG_STR("a long string that can't use Small String Optimization");

void PassStringByValue(benchmark::State& state) {
for (auto _ : state) {
size_t n = GetSpaces_Value(LONG_STR);
}
}

void PassStringByRef(benchmark::State& state) {
for (auto _ : state) {
size_t n = GetSpaces_Ref(LONG_STR);
}
}

//----------------------------------
size_t Sum_Value(std::vector<unsigned> vect)
{
size_t sum = 0;
for(unsigned val: vect) { sum += val; }
return sum;
}

size_t Sum_Ref(const std::vector<unsigned>& vect)
{
size_t sum = 0;
for(unsigned val: vect) { sum += val; }
return sum;
}

const std::vector<unsigned> vect_in = { 1, 2, 3, 4, 5 };

void PassVectorByValue(benchmark::State& state) {
for (auto _ : state) {
size_t n = Sum_Value(vect_in);
}
}

void PassVectorByRef(benchmark::State& state) {
for (auto _ : state) {
size_t n = Sum_Ref(vect_in);
benchmark::DoNotOptimize(n);
}
}

명백하게, passing by reference의 성능이 더 좋습니다.




Const reference를 쓰면 안되는 예시

“오옹… 앞으로는 const&로 내 코드를 도배해야겠다~”

이런 생각을 하기 전에, 다음 예제도 한번 봅시다.

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
struct Vector3D{
double x;
double y;
double z;
};

Vector3D MultiplyByTwo_Value(Vector3D p){
return { p.x*2, p.y*2, p.z*2 };
}

Vector3D MultiplyByTwo_Ref(const Vector3D& p){
return { p.x*2, p.y*2, p.z*2 };
}

void MultiplyVector_Value(benchmark::State& state) {
Vector3D in = {1,2,3};
for (auto _ : state) {
Vector3D out = MultiplyByTwo_Value(in);
}
}

void MultiplyVector_Ref(benchmark::State& state) {
Vector3D in = {1,2,3};
for (auto _ : state) {
Vector3D out = MultiplyByTwo_Ref(in);
}
}

의외로, 이번에는 const&를 써도 아무 차이가 없습니다.

우리가 heap allocation이 필요없을만큼 작은 (그냥 몇 바이트 정도로 작은 데이터) 객체를 복사하는거면, passing by reference를 써도 거의 차이가 없습니다. 하지만 그렇다고 더 느려지는것도 아니죠. 그러니까 const&를 써야할지 말지 모르겠을때는, 일단 써보면 좋을 것 같습니다.

다만 문제가 되는 경우도 있습니다.

Primitive types를 const reference로 복사할 때는 컴파일러가 추가적인 instruction을 더 만든다고 하지만 (여기서 테스트 해봤습니다 https://godbolt.org/z/-rusab), 하지만 컴파일 옵션에 -O3 를 추가하면 이 문제 역시 해결됩니다. 하지만 매번 이 옵션을 넣어주긴 귀찮으니, 저는 보통 ‘8 바이트 정도 되는 인자, 또는 그것보다 작은 인자 (e.g. int, double, chars, long)`은 절대로 pass by reference를 쓰지 않습니다. Primitive type에 const, & 등을 사용하면, 성능 향상은 하나도 되지 않으면서, 동시에 코드가 더 헷갈려보이고 못생겨집니다.

1
void YouAreTryingTooHardDude(const int& a, const double& b); // 이건 하지 맙시다





게시글 공유:Twitter, Facebook, and LinkedIn