스택 메모리 상의 placement new
임의의 함수 객체를 저장하는 클래스를 작성하였습니다. 임의의 함수 객체의 크기는 예측할 수 없기 때문에 원래의 코드에서는 함수 객체의 크기만큼 힙 메모리를 할당한 후에 함수 객체를 복사하여 저장하였습니다. 그런데 힙 메모리 할당이 특정 상황(!) 에서는 성능 저하에 영향을 줄 수 있기 때문에 메타 템플릿 기법을 사용하여 특정 크기( 2 * sizeof(void *) )의 unsigned char 버퍼를 멤버로 선언하고 함수 객체의 크기가 내장 버퍼 사이즈 보다 작을 경우에는 특별히 placement new 를 사용하여 힙 메모리 할당을 하지 않음으로써 속도 향상을 얻고자 했습니다. 실재 위의 클래스를 적용하는 환경에서 임의의 클래스 T가 간단한 prediate 이거나 단순히 free 함수를 감싸고 있는 경우가 대부분 인지라서 기대 이상의 속도 향상을 예상하고 있었습니다.
그러다가 C++ 표준에 호환하는 코드를 작성하고자 한다면 placement new 를 스택 메모리에 할당 된 버퍼에 사용하면 안된다는 것을 조금 뒤늦게서야 깨우치게 되었습니다. ( http://www.devx.com/tips/Tip/12756 또는 http://www.parashift.com/c++-faq-lite/dtors.html#faq-11.10 ). 여기 포럼을 검색한 후 정렬 제한 때문에 비인텔 계열의 CPU에서는 BUS 에러가 발생한다는 사실을 어렴풋이 알게 되었습니다.
인터넷을 조금 더 검색하다 보니 다음의 포럼에서 http://www.dbforums.com/showthread.php?t=1556368 끝 부분에서 올라온 답변에 Frederick Gotham란 사람이 세 가지 대안 방법을 제시 하였습니다.
(1) Dynamically allocating it
(2) Using union trickery
(3) Using boost's "align_for" (or whatever it's called)
세 번째 boost 의 경우는 type traits 라이브러리 중 alignment_of, aligned_storage, type_with_alignment 가 이 사람이 제시한 방법을 의미하는 것 같습니다. 반면에 두 번째 공용체 트릭이라는게 어떤 것인지 검색해 봤는데 찾을 수가 없었습니다.
정렬 제한이 위의 경우에 어떻게 적용되는지 정확하게 개념이 서지 않고 있습니다. 자세한 설명과 함께 사용 가능한 (그러면서 표준에 부합하는 방법이면 좋겠지만 그렇지 않다면 비인텔 계열에서도 문제가 발생하지 않는) 방법을 알려주시면 정말로 감사 드리겠습니다.
struct functor_ptr_holder { union { void * ptr_; //!< Pointer to the function object allocated on the heap memory. unsigned char buf_[2 * sizeof( void * )]; //!< Buffer to store the function object if the size fits. }; // union typedef bool (*typeless_deleter_stub)(functor_ptr_holder &); typeless_deleter_stub deleter_stub_; //!< Deleter stub. template<class T> struct on_stack_ { inline static void init(functor_ptr_holder & fph, T * ptr) { new ( fph.buf_ ) T( *ptr ); fph.deleter_stub_ = &on_stack_<T>::deleter; } inline static void deleter(functor_ptr_holder & fph) { if( ptr ) reinterpret_cast<T *>( &fph.buf_ )->~T(); } }; // template<class T> struct on_stack_ template<class T> struct on_heap_ { inline static void init(functor_ptr_holder & fph, T * ptr) { fph.ptr_ = new T( *ptr ); fph.deleter_stub_ = &on_heap_<T>::deleter; } inline static void deleter(functor_ptr_holder & fph) { if( ptr ) delete static_cast<T *>( fph.ptr_ ); } }; // template<class T> struct on_heap_ template<class T> struct select_ { typedef on_stack_<T> then_type; typedef on_heap_<T> else_type; typedef typename if_< sizeof( T ) <= buffer_size, then_type, else_type >::type type; }; // template<class T> struct select_ // -------------------------------------------------------------------------------- functor_ptr_holder() : ptr_( 0 ), deleter_stub_( 0 ) { } template<class T> functor_ptr_holder(T * ptr) : ptr_( 0 ), deleter_stub_( 0 ) { select_<T>::type::init( *this, ptr ); } }; // struct functor_ptr_holder
관련된
관련된 링크입니다.
http://www.gotw.ca/gotw/028.htm
___________________________________
Less is More (Robert Browning)
___________________________________
Less is More (Robert Browning)
감사합니다.
링크 정말 감사드립니다. 원하는 답변이 설명 되어있습니다. boost 의 alignment_of 등등의 구현도 조금 더 이해하기가 수월해졌습니다. 예전에 gotw 시리즈 한번 훝어보면서 봤었던 내용인데 막상 필요해서 찾을때는 기억을 못해냈네요.
감사합니다.
질문이 나온김에...
일반 함수 또는 멤버 함수를 저장할 수 있는 C++ delegate 클래스를 작성한적이 있습니다. 힙 메모리 할당을 하지 않기 위해서 위에서와 마찬가지로 char 배열 버퍼를 멤버로 잡고서 placement new를 사용하여 boost::function 보다 수 배 빠른 성능 향상을 얻을 수 있었습니다만 이제서야 이와 같은 접근이 정렬 제한을 만족하지 못하고 따라서 특정 환경에서 버스 오류를 발생시킬 수 있다는 것을 알게 되었습니다.
위의 GOTW 에서 설명된 공용체 트릭을 조금 변경해서 다음과 같이 수정하면 표준에 위배되지 않는다고 할 수 있을지 알고 싶습니다. "함수를 내장 char 버퍼에 저장한다는 특수한 상황"이기 때문에 다음과 같은 공용체를 사용함으로써 표준에 완벽하게 부합한다고 생각합니다.
템플릿 메타 프로그래밍을 사용하여 다중 상속을 받은 클래스의 멤버 함수 포인터의 크기보다 작거나 같은 함수 포인터들은 위와 같이 정의된 내장된 버퍼에 저장하고 크기가 큰 경우에만 힙 메모리 할당을 하여 저장하는 방식을 사용합니다. 코드 프로젝트의 http://www.codeproject.com/cpp/FastDelegate.asp 의 중반에 나온 함수 비교 테이블에 참조 했습니다.
이와 같은 접근 방법이 C++ 표준에 전혀 위배되지 않는다고 말할 수 있을까요?
댓글 달기