[완료]객체를 반환하는 함수를 만들때 임시객체 생성에 의한 오버헤드 줄이기
글쓴이: simnalamburt / 작성시간: 토, 2010/12/11 - 10:12오후
문자열을 반환하는 함수가 있다고 합시다.
#include <stdio.h> #include <stdlib.h> #include <string> std::string GetString() { return std::string("문자열문자열문자열"); } int main() { std::string szTmp; szTmp = GetString(); puts(szTmp.c_str()); system("PAUSE"); return 0; }
컴파일 하면 아주 정상적으로 작동합니다만,, 지금 있는 GetString()의 문제는
함수가 시행될때마다 std::string 임시객체가 생성되고 사라지고를 반복한다는 것이지요.
szTmp 쪽에서도 반환된 임시객체를 복사하느라 또 연산이 필요하고 오버헤드가 발생합니다
이런 경우는 단순한 예라서 별로 문제는 없겠지만...
std::vector<long long int> GetVector() { std::vector<long long int> Tmp; Tmp.assign(0,10000000); return Tmp; }
이런식으로 거대 객체를 반환하거나 할때는 이야기가 달라지겠지요..
웬만한 컴파일러는 반환 최적화를 자동으로 하기때문에 이런 간단한 경우는 별 문제가 없는걸로 압니다만
컴파일러마다 다르다니 별로 마음에 들지 않는군요 ㅡㅡㅋ;;
C++0x 표준에서 새롭게 지원하는 r-value reference를 이용하면 이런 문제를 해결할 수 있는걸로 알지만
그 신 표준을 접한지 오래되지 않아 응용하는법을 잘 모르겠군요;;;
도움줄 수 있으신분 있으시면 감사하겠습니다 +_+
Forums:
말씀하신대로 컴파일러 최적화도 있지만, 보통 복사
말씀하신대로 컴파일러 최적화도 있지만, 보통 복사 오버해드가 큰 경우는 implicit data sharing이라는 기법을 씁니다.
복사할때는 포인터만 복사하고, 실제 내용의 복사는 객체에 수정이 가해질때(주로 non-const 함수가 호출될때)에 이루어집니다.
따라서 implicit data sharing을 이용하면 임시객체로의 복사 오버해드는 무시가능한 수준이 됩니다.
다만, 일반적으로 이것은 reference counting같은 기술을 이용하기 때문에, 아주 단순한 객체에 이것을 적용하면, 오히려 손해일수 있습니다.
r-value에 관해선...
말씀하신대로 한번만 복사되고 없어질 임시 객체에 대해 reference counting을 쓰는것도
경우에 따라 손해가 될 수 있을것 같군요..
http://rein.kr/blog/archives/2371
이 글에 있는 프리젠테이션을 보니 (슬라이드 29~41)
r-value 참조를 이용해서 거대벡터 반환할때의 오버헤드를 줄일 수 있다는군요
그런데 할수 있다고만 나와있고 어떻게 하는지는 설명이 되어있지 않습니다.
혹시 r-value 참조를 이용해서 하는 방법에 대해서 알고계신 부분이 있으신가요??
당연히 어떤 기술을 적용하면, 특히나 라이브러리
당연히 어떤 기술을 적용하면, 특히나 라이브러리 차원에서의 구현이라면, 반드시 그에따른 오버해드가 존재합니다.
다만 일반적인 상황에서, 예를 들어 벡터의 복사를 생각할 때 reference counting으로 인한 손해와, 모든 데이터를 복사함으로서 생기는 손해를 비교해보면, reference counting으로 인한 오버해드따위 무시가능한 수준이 됩니다.
무조건 유리한 쪽으로만 작용하는 것은 없으니, 비교해보고 그래도 이쪽이 이득이라고 생각되면 채택하고, 아니라면 채택하지 않는 것이지요.
예를 들어 아무리 커다란 객체라고 해도 사실상 복사될 일이 없다면(반드시 포인터로만 사용한다던가...), implicit data sharing따위 필요없겠지요.
반대로, 일반적으로 문자열은 복사가 매우 빈번히 일어나기 때문에, 보통 문자열이 그리 큰 메모리를 차지하지 않는다고 해도, 문자열 클래스에는 implicit data sharing을 적용하는게 좋겠지요.
r-value reference는 제가 지금까지 보아왔던 C++0x명세에서는 보지 못했던 것이네요.
나중에 추가된 것인지, 단순히 제가 본 정보가 부족했던 것인지 모르겠지만, 저도 처음보기 때문에 아직 잘 모르겠습니다.
rvalue reference 는 c+++0x 의 핵심이죠.
저도 C++을 사용할때 늘 객체 복사가 신경쓰이더군요.
C++0x 의 rvalue reference 가 그간의 이런 고민들을 대부분 해결할 수 있는 방법이 되리라 생각합니다.
여기 최홍배 MVP님이 만드신 멋진 문서 올립니다. kldp에서 본 것 같은데, 못찾겠네요.
rvalue reference 의 더 자세한 부분은 문서의 ref를 참조하시면 될것 같네요.
더 자세한 rvalue ref 사용방법은 이 링크 참고하시면 될것 같습니다.
* http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx
* http://blogs.msdn.com/b/msdnforum/archive/2010/03/19/rvalue-reference-support-in-visual-c-2010.aspx
단순한 코드에 대한 STL의 벤치마크가 인상깊네요.
* http://cpp-next.com/archive/2010/10/howards-stl-move-semantics-benchmark/
방법을 알았습니다
조금더 공부를 해보니
함수는 저런 형태로 놔두고
함수를 호출한 쪽에서 move-sementic을 써주면 되는거였습니다
정확히 말하면 move-sementic을 써주면 되는게 아니라 STL 라이브러리에 새로 추가된 vector 생성자때문에 자동으로 써지게 되더군요(?!)
임시객체를 받아 저장하는 쪽에선 함수를 이렇게 호출하지요?
실은 r-value 참조가 지원되기 전에도 이렇게 썼었지요.
r-value 참조를 써서 오버헤드를 줄이려면...
C++0x 신표준이 지원되는 컴파일러로 이 코드를 똑같이 쓰면 됩니다 (?!)
종래에 이런 방식으로 쓰면
1. 임시객체 생성됨
2. 빈 객체 Copy가 생김
3. 객체 Copy에 임시객체의 데이터가 하나하나 복사됨
4. 임시객체 파괴됨
이런 원치않는 과정이 발생했었습니다. r-value 참조가 지원되지 않았었기 때문에 GetVector()의 형식을 보고
이녀석이 호출되었었기 때문이죠.
주석에도 _Right를 복사함으로써 벡터가 생성된다고 명시되어있습니다.
그런데 C++0x 이후 STL에 새롭게 추가된 생성자중에
이런녀석이 있습니다. _Myt&& <- r-value 참조기호지요.
auto Copy = GetVector(); 이런식으로 함수를 호출하면
GetVector()의 반환값이 const T& 가 아니라 T&&으로 먼저 인식되기때문에
move sementic으로 생성됩니다.
더 대단한건 생성자뿐이 아니라 연산자=, assign 메소드 등등에서
r-value 를 지원하는것이 모두 새로 추가되었기때문에
r-value 참조로 오버헤드를 줄이고 싶다면
그냥 컴파일러를 C++0x 지원되는것으로 바꾸면 됩니다 (...)
단 벡터와 같은 STL이 아닌
사용자가 임의로 만든 클래스를 반환하거나 할때에
r-value 참조를 사용한 생성자, 연산자= 를 만들 줄 모른다!
그럴때엔 l-value 참조하듯이 종래와 같은 방법으로 생성자, 연산자=를 만든다음
함수를 호출한 쪽에서
이런식으로 쓰면 모든게 해결됩니다.
std::move는 사용자가 만든 클래스에서도 move-sementic을 쓸 수 있도록
만든 함수 템플릿입니다. 메모리가 지정하는 위치만 바꿔주기 때문에
다들 아시다시피 메모리의 크기와 관계없이 아주 빠르지요.
1. 임시객체 생성됨
2. 빈 객체 Move가 생김
3. 빈 객체 Move와 임시객체가 가리키는 곳이 뒤바뀜
-> Move는 거대벡터를 가리키게되고, 임시객체가 가리키는 곳은 빈 객체가됨
4. 비어버린 임시객체 소멸됨
이렇게 하면
a. 복사하느라 드는 비용이 줄고
b. 거대한 임시객체를 해제하는 비용이 줍니다
**********
혹시 몰라 F11로 한줄한줄 코드를 실행시켜봤는데
생성자 vector(_Myt&& _Right) 가 호출되는것을 보고 깜짝 놀랐습니다
역시 C++은 만만한 언어가 아니군요
반드시 알아둬야겠습니다.
std::move에 대해 조금 의문이 듭니다.
제가 알기론 std::move()는 Rvalue reference가 되도록 캐스팅 해주는 녀석입니다.
헌데, 임의로 만든 클래스 말씀하신 부분에서
라고만 하면 모든게 해결된다고 하셨는데, 복사 생성자와 마찬가지로 '이동 생성자'도 그 방법을 객체가 알고 있어야 하는 것으로 압니다.
즉,
라고 했을 때 GetObject() 리턴값은 Rvalue reference이므로 Object에 '이동 생성자'가 있다면 그것이 호출되며, 없으면 '복사 생성자'가 호출되어야 합니다.
Object에 이동 생성자가 정의되어있지 않다면 복사 생성자가 호출되겠지요.
GetObject()의 반환값 자체가 Rvalue reference인데, std::move() 역시 Rvalue reference로 캐스팅 해주는 녀석이기 때문에 붙이나 안 붙이나 아무 효과가 없습니다.
move semantics란 것은 그 클래스가 지원을 해야 한다는 것이죠.
따라서, 기존 라이브러리에 복사 생성자만 있고 이동 생성자가 없을 경우에 std::move()를 써도 소용이 없습니다.
Object에 이동 생성자를 만들기 전까지는 Object는 move semantics가 없는 것입니다.
STL의 구성요소들은 move sematics를 지원하기 때문에 별도의 조치 없어도 자동으로 처리가 되는 것이구요.
그럼 std::move()는 언제 써야 할까요?
이건 Lvalue reference를 Rvalue reference로 바꿔줄 때 써야 합니다.
즉,
m이 Lvalue reference이므로 Object에 이동 생성자가 있더라도 두번째 줄은 복사 생성자가 호출됩니다.
이때 방금 수행한 대입(초기화)이 copy semantics가 아니라 move semantics라고 표현해주기 위해 std::move()를 사용합니다.
전자는 copy semantics므로 m의 값은 변하지 않지만, 후자는 move semantics이므로 (이동 생성자에서 구현한 대로) n을 초기화 한 뒤 m의 값은 바뀌게 됩니다.
물론, Rvalue reference에 std::move()를 사용해도 지장 없습니다. 다만 굳이 쓸 필요가 없습니다.
Rvalue reference는 주로 이름 없는 임시객체기 때문에, a) 함수 리턴값 b) 연산자 함수의 결과 c) 생성자로 만든 임시객체, 들은 알아서 move semantics가 적용되고, 그건 문법적으로 간단히 알아볼 수 있기 때문에 생략해도 될 것 같습니다.
혹시 제가 이해한 것이 틀렸다면 지적 부탁드립니다.
댓글 달기