C++ 효율 향상 시키기 (feat. 임시 객체)
본 포스트는 More Effective C++ item 19 ~ 22 를 참고하여 작성함.
임시 객체의 origin을 이해하자
임시 객체 (Temporaries) ?
template<class T>
void swap(T& object1, T& object2)
{
T temp = object1;
object1 = object2;
object2 = temp;
}
여기에 있는 이 temp
말하는거 아님!!
HON MONO 임시 객체는 코드에도 없고, 눈에 보이지 않음
heap이 아닌 곳에 생기는 un-named object 인데.... 언제 생기냐?
1. When implicit type conversions (암시적 타입 변환) are applied to make function calls succeed.
size_t countChar(const string& str, char ch);
char buffer[MAX_STRING_LEN];
countChar(buffer, c)
buffer
는 char*
-> string
으로 타입 변환이 일어날 때, compiler가 친히 string 임시 객체를 만들어 줌.
그러나.....
void uppercasify(string& str); // str을 전부 대문자로 바꿈
char subtleBookPlug[] = “Effective C++”;
uppercasify(subtleBookPlug); // ERROR!
subtleBookPlug
는 char*
-> string
가 안되고 컴파일 에러가 남.
왜냐하면, 생성된 임시객체에 수정을 가하는 것이 원 객체에 영향을 미쳐야 하기 때문!
reference-to-non-const는 안됨!!
2. When functions return (함수 반환) objects.
const Number operator+(const Number& lhs,
const Number& rhs);
a+b
결과물이 임시객체로 생성되어서 c
로 복사됨.
반환값이 이름이 없으니 어쩔수 없음
어쨋든 문제는 계속 임시객체가 생기면서 비용이 증가함.
어디서 생성되는지 통찰하고, 어떻게 최소한으로 할 것인지 이제 소개함!
이게 책의 내용인데..... 실제로는 이러한 임시객체가 생기지 않는 것으로 보임.
진짜로 생기는지 의문이 들어서 직접 테스트 해본 결과는 맨 아래(테스트 결과)에 있음.
Return Value Optimization 활용하기
const Rational operator*(const Rational& lhs,
const Rational& rhs);
이렇게 객체를 값으로 반환하는 것은 효율을 생각하면 개똥임. 맨날 임시객체가 생기니까...
그럼 어케 최적화하지...
흐음....
ㅠㅠㅠㅠㅠ
포인터?!
const Rational* operator*(const Rational& lhs,
const Rational& rhs);
포인터로 반환하면 임시객체가 안생기지~
그러나,
이걸 써서 코딩하면???
Rational a = 10;
Rational b (1,2);
Rational c = * (a * b);
부자연스럽네 ㅠㅠ
시무룩....
그렇다면,
레퍼런스?!
const Rational& operator*(const Rational& lhs,
const Rational& rhs);
레퍼런스로 반환해도 임시객체가 안생기지~
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
return result;
}
문제는 result
가 저 함수 끝나고 스택에서 지워져서 못써먹음 ㅠㅠㅠ
value를 리턴하는거는 어쩔수 없구만! 흑흑
그러면, 그 cost라도 줄여야겟어!
const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
이게 정답이라는데 왜 일까요?
Rational c = a * b;
실행시,
컴파일러는 operator*
안에서 만드는 임시객체와 반환하는 임시객체를 전부 없애고 return
expression을 Rational c
에 할당된 메모리 안에 직접 초기화 해줌.
컴파일러 짱짱맨!
inline const Rational operator*(const Rational& lhs,
const Rational& rhs);
이렇게 inline
붙이면 call하는 overhead 도 줄일 수 있음!
결론: values return 때에는 constructor를 사용하자
오버로딩으로 암시적 타입변환을 막아보자
class UPInt { // 무한을 품은 인트
public:
UPInt();
UPInt(int value);
...
};
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
UPInt upi1, upi2;
...
UPInt upi3 = upi1 + upi2;
upi3 = upi1 + 10;
upi3 = 10 + upi2;
upi3 = 10 + upi2;
요런데서 10 을 UPInt
로 암시적 타입변환을 함.
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
이거를 쓰려고, 임시객체가 자꾸자꾸 생기는 현상이 발생 ㅠㅠ
const UPInt operator+(const UPInt& lhs, const UPInt& rhs);
const UPInt operator+(const UPInt& lhs, int rhs);
const UPInt operator+(int lhs, const UPInt& rhs);
const UPInt operator+(int lhs, int rhs);
이렇게 다 만들어버리면 임시객체 안생기지롱~~
신나게 오버로딩하다보니 주화입마에 빠져버렷다ㅠㅠ
저기에는 x맨이 끼어있음.
누굴까요??
잡앗다 요놈!
const UPInt operator+(int lhs, int rhs);
컴파일에러임.
왜냐면, '오버로딩되는 연산자 함수는 반드시 최소한 한 개의 사용자 정의 타입을 매개변수로 가져야한다' 라는 규칙이 있음.
어쨌든, 80-20 룰에 따라 많이 불려야 하는 놈이면 이런걸 하는 것도 좋은 방법이다!
"=" 말고 "+=" 를 고려하자
x = x + y;
x = x - y;
이게 된다면!
x += y;
x -= y;
이것도 되는게 프지상정!!
(책에 있는 표현임. 내가 만든거 아님.)
구현을 해보자.
class Rational {
public:
...
Rational& operator+=(const Rational& rhs);
};
// operator+ 는 operator+=를 이용하여 구현
const Rational operator+(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs) += rhs;
}
이 때,
Rational& operator+=(const Rational& rhs);
는 레퍼런스로 반환이니까, 임시객체가 안생기고 (!!),
const Rational operator+(const Rational& lhs, const Rational& rhs)
는 값을 반환하니까 임시객체가 생김.
그럼, +=
가 좋은거네!
하지만...
result = a + b + c + d;
이렇게 쓰면 될 놈이,
result = a; result += b; result += c; result += d;
이렇게 써야함 ㅠㅠㅠㅠ
장단이 있음.
그리고, 구현을 할 때는
const Rational operator+(const Rational& lhs,
const Rational& rhs)
{
T result(lhs); // lhs를 result에 copy
return result += rhs;
}
이렇게 하지 말고,
const Rational operator+(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs) += rhs;
}
이렇게 해야 compiler optimization이 잘 된다! 아까 배운거임.
- 일반적으로 op= 형태가 op 보다 효율적
- 라이브러리 설계자는 두 형태 모두 제공해야 하겠다
- 효율이 우선인 개발자는 op 형태가 아닌 op= 형태를 쓰는 게 좋다
끝낼라고 했는데, 실제 테스트 해보니 위의 내용이 틀렸다고 생각 됨. 아래 단락 참조할 것!
테스트 결과
책에서는 value로 return 할 때, 임시객체가 만들어 진다고 하였다. 스택에 있던게 옮겨가니 그런가 보다 했는데, 또 return을 생성자로 하면 optimization 된덴다. 흐음... 이게 좀 이상한거 같아서 테스트를 해보았다. 요즘 컴파일러는 알아서 optimization이 잘 되는 거 같다.
테스트 환경
gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC)
테스트 코드
- class Number 정의
class Number{
public:
Number(){
n_ = 0;
};
Number(int n){
n_ = n;
};
~Number(){
};
Number(const Number& other){
n_ = other.n_;
};
int get() const {
return n_;
};
Number& operator=(const Number& other){
n_ = other.n_;
}
private:
int n_;
};
- main 코드.
int main(){
Number a(1);
Number b(2);
Number c;
c = a*b;
cout<<c.get()<<endl;
return 0;
}
- 최적화가 안될 operator. 책에서 말하는 안좋은 예시.
const Number operator*(const Number& lhs, const Number& rhs){
Number result(lhs.get()*rhs.get());
return result;
};
- 최적화가 된 operator. 책에서 좋은 예시.
const Number operator*(const Number& lhs, const Number& rhs){
return Number(lhs.get()*rhs.get());
};
1번의 경우에 c = a*b;
수행시 a*b
의 결과가 c
로 복사될때 임시객체가 생겼다가 없어져야 하고, 2번의 경우에는 그렇지 않아야 한다. 실제로 그런지 한번 보자.
아래 asm 창에서 보다 시피, operator *
이후에 operator =
, ~Number()
가 불린다. operator *
결과 반환을 위해 임시 객체는 생성되지 않는다.
opeator*
내부에서도 result
객체 생성자 이외에 다른 생성자가 불리지 않는다.
그렇다면, stack 에 생성된 result
는 어떻게 없어지지 않고 위로 올라갓다가 다시 operator=
으로 전달이 되는가??
main
의 stack. operator*
를 call하는 시점.
$rbp = 0x7fffffffdbe0
, $rsp = 0x7fffffffdbc0
이다.
다음은 operator*
의 stack. result
를 반환하는 시점.
$rbp = 0x7fffffffdbb0
, $rsp = 0x7fffffffdb80
이다.
result
객체의 주소는 0x7fffffffdbcc
. 이 스택에 생긴다.
다음은 다시 main
스택. operator=
을 부르는 시점.
$rbp = 0x7fffffffdbe0
, $rsp = 0x7fffffffdbc0
이다.
여기서, result
가 아직 지워지지 않았음. 분명히 스택을 빠져 나왔는데!!
다음은 operator=
의 stack. c
에 result
를 대입하는 시점.
$rbp = 0x7fffffffdbb0
, $rsp = 0x7fffffffdba0
이다.
result.n_
을 $edx
에 로드해서, ($rax) = c.n_
에 대입함.
그리고 나서, result
의 destructor 가 호출됨.
- 참고: 그림 잘 못 그렷네 ㅠㅠ 저 result 는 main stack 안에 잡혀있음. 위치가 틀림.
이와 같은 상황으로, 중간에 stack을 나가는데도, result를 지우지 않음.
결론은, 1번의 경우에 임시객체가 생성 되지 않음.
신기하구만!