C++ 효율 향상 시키기 (feat. 임시 객체)

Share
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);

이렇게 객체를 값으로 반환하는 것은 효율을 생각하면 개똥임. 맨날 임시객체가 생기니까...
그럼 어케 최적화하지...

고민

흐음....

고민고민

ㅠㅠㅠㅠㅠ

sadpepe

포인터?!

const Rational* operator*(const Rational& lhs,
		          const Rational& rhs);

happypepe

포인터로 반환하면 임시객체가 안생기지~

그러나,

이걸 써서 코딩하면???

Rational a = 10;
Rational b (1,2);
Rational c = * (a * b); 

부자연스럽네 ㅠㅠ

pepepe

시무룩....

그렇다면,

레퍼런스?!

const Rational& operator*(const Rational& lhs,
		          const Rational& rhs);

happypepe

레퍼런스로 반환해도 임시객체가 안생기지~

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	Rational result(lhs.numerator() * rhs.numerator(),
	lhs.denominator() * rhs.denominator());
	return result;
}

문제는 result가 저 함수 끝나고 스택에서 지워져서 못써먹음 ㅠㅠㅠ

pepepe

value를 리턴하는거는 어쩔수 없구만! 흑흑

mpepe

그러면, 그 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에 할당된 메모리 안에 직접 초기화 해줌.

컴파일러 짱짱맨!

happypepe

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맨이 끼어있음.

누굴까요??

podol

잡앗다 요놈!

const UPInt operator+(int lhs, int rhs);

컴파일에러임.

왜냐면, '오버로딩되는 연산자 함수는 반드시 최소한 한 개의 사용자 정의 타입을 매개변수로 가져야한다' 라는 규칙이 있음.

어쨌든, 80-20 룰에 따라 많이 불려야 하는 놈이면 이런걸 하는 것도 좋은 방법이다!

"=" 말고 "+=" 를 고려하자

x = x + y;
x = x - y;

이게 된다면!

rocket1

x += y;
x -= y;

이것도 되는게 프지상정!!
(책에 있는 표현임. 내가 만든거 아님.)

rocket2

구현을 해보자.

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이 잘 된다! 아까 배운거임.

  1. 일반적으로 op= 형태가 op 보다 효율적
  2. 라이브러리 설계자는 두 형태 모두 제공해야 하겠다
  3. 효율이 우선인 개발자는 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;
}
  1. 최적화가 안될 operator. 책에서 말하는 안좋은 예시.
const Number operator*(const Number& lhs, const Number& rhs){
  Number result(lhs.get()*rhs.get());
  return result;
};
  1. 최적화가 된 operator. 책에서 좋은 예시.
const Number operator*(const Number& lhs, const Number& rhs){
  return Number(lhs.get()*rhs.get());
};

1번의 경우에 c = a*b; 수행시 a*b의 결과가 c로 복사될때 임시객체가 생겼다가 없어져야 하고, 2번의 경우에는 그렇지 않아야 한다. 실제로 그런지 한번 보자.

Screenshot-from-2018-09-06-18-10-49

아래 asm 창에서 보다 시피, operator * 이후에 operator =, ~Number()가 불린다. operator * 결과 반환을 위해 임시 객체는 생성되지 않는다.

Screenshot-from-2018-09-06-18-13-49

opeator* 내부에서도 result 객체 생성자 이외에 다른 생성자가 불리지 않는다.
그렇다면, stack 에 생성된 result 는 어떻게 없어지지 않고 위로 올라갓다가 다시 operator=으로 전달이 되는가??

main의 stack. operator*를 call하는 시점.
stack1
$rbp = 0x7fffffffdbe0, $rsp = 0x7fffffffdbc0 이다.

다음은 operator*의 stack. result를 반환하는 시점.
stack2
$rbp = 0x7fffffffdbb0, $rsp = 0x7fffffffdb80 이다.
result 객체의 주소는 0x7fffffffdbcc. 이 스택에 생긴다.

다음은 다시 main 스택. operator= 을 부르는 시점.
stack3-1
$rbp = 0x7fffffffdbe0, $rsp = 0x7fffffffdbc0 이다.
여기서, result가 아직 지워지지 않았음. 분명히 스택을 빠져 나왔는데!!

다음은 operator=의 stack. c 에 result를 대입하는 시점.
stack4
$rbp = 0x7fffffffdbb0, $rsp = 0x7fffffffdba0 이다.
result.n_을 $edx 에 로드해서, ($rax) = c.n_ 에 대입함.

그리고 나서, result의 destructor 가 호출됨.

stack_figure

  • 참고: 그림 잘 못 그렷네 ㅠㅠ 저 result 는 main stack 안에 잡혀있음. 위치가 틀림.

이와 같은 상황으로, 중간에 stack을 나가는데도, result를 지우지 않음.

결론은, 1번의 경우에 임시객체가 생성 되지 않음.

신기하구만!

Read more

허깅페이스 르로봇 해커톤 후기

허깅페이스 르로봇 해커톤 후기

본 블로그의 주인장들은 이번 허깅페이스의 르로봇 월드와이드 해커톤의 서울 호스트를 맡았습니다. 너무 재밌었고, 그 과정들과 후기를 남깁니다. 현장은 다큐 형식의 작은 영상을 만들었으니 한번 봐주세요. 계기 저는 LLM 만 하고 있었고, Action 모델은 사실 관심이 없었습니다, 준호는 1년 정도 로봇에 AI 를 넣는 일을 하고 있었죠. 허깅페이스에서 르로봇 플랫폼을 만들고

By park jong hyun
DeepDive LLM 3편 - Reinforcement Learning

DeepDive LLM 3편 - Reinforcement Learning

* 1편 - 사전 학습 (Pre Training) * 2편 - Supervised Fine Tuning * 3편 - Reinforcement Learning Reinforcement Learning Pretrain, SFT 에 이어서 세번째 단계는 Reinforcement Learning (RL, 강화학습) 입니다. 비유를 해보자면, pretrain은 그냥 책을 읽는 것이고요, SFT는 예제 문제와 이미 작성된 해설을 보는 것입니다. RL 은 해설이 없는 문제를 직접 풀어보는 것입니다. 어떻게 풀든

By park jong hyun
DeepDive LLM 2편 - Supervised Fine Tuning

DeepDive LLM 2편 - Supervised Fine Tuning

* 1편 - 사전 학습 (Pre Training) * 2편 - Supervised Fine Tuning * 3편 - 강화 학습 (Reinforcement Learning) Supervised Fine Tuning Post Training 의 첫번째 단계 SFT 입니다. Pre Training 에 비하면 아주 작은 양의 데이터만 필요하지만, 실제로 성능을 이끌어 내는데에는 중요한 단계입니다. 멀티턴 대화 (Multi Turn Conversation) ChatGPT 를 포함해서 대부분의 AI 서비스들은

By park jong hyun
DeepDive LLM 1편 - 사전 학습 (Pre-Training)

DeepDive LLM 1편 - 사전 학습 (Pre-Training)

본 포스트 시리즈는 카파시 선생님의 영상 을 기반으로 정리한 내용입니다. 직접 유투브에서 다루기도 했으니 영상으로 보셔도 됩니다. * 1편 - 사전 학습 (Pre-Training) * 2편 - Supervised Fine Tuning * 3편 - 강화 학습 (Reinforcement Learning) LLM 의 Training LLM을 만들 때는 크게 세 가지 주요 단계가 있습니다. 1. 사전 학습(Pre-Training) * 인터넷 텍스트(

By park jong hyun