(까막님의 downcast overloading 글에서 아이디어를 얻어 작성하였습니다.)
이전에 MemPool 클래스를 작성할 때 마음엔 안들지만 방법을 찾지 못해 그냥 두었던 부분이 하나 있었습니다.
실제 코드를 읽기 쉽게 간단히 다시 써보았습니다. ((log_2와 power_2는 다음과 같이 간단히 구현되어 있습니다.
성능상 기존 방법에 비해 함수 호출에 따른 overhead가 조금 있을 수 있겠지만 매번 호출되는 것이 아니라 각 Impl에 대해 한번씩만 호출되는 것이므로 문제는 없어보입니다. 이런 overhead에 신경쓸 사람도 많지 않겠지만요. :-)
이전에 MemPool 클래스를 작성할 때 마음엔 안들지만 방법을 찾지 못해 그냥 두었던 부분이 하나 있었습니다.
실제 코드를 읽기 쉽게 간단히 다시 써보았습니다. ((log_2와 power_2는 다음과 같이 간단히 구현되어 있습니다.
template <int n>
struct log_2
{
BOOST_STATIC_ASSERT(N % 2 == 0);
enum {
value = 1 + log_2<n / 2>::value
};
};
template <>
struct log_2<2>
{
enum {
value = 1
};
};
template <std::size_t n>
struct power_2
{
enum {
value = 2 * power_2<n - 1>::value
};
};
template <>
struct power_2<0>
{
enum {
value = 1
};
};
))struct Base
{
};
template <int n>
class Impl : public Base
{
};
template <int max_size, int min_size>
class Allocator
{
public:
static Base* get_impl(int index) {
const int POOL_CNT = log_2<max_size, min_size>::value + 1;
static Base* impl_[POOL_CNT] = { 0 };
Base* ret = impl_[index];
if (ret == 0) {
switch (index) {
case 0: impl_[index] = new Impl<min_size * power_2<0>::value>; break;
case 1: impl_[index] = new Impl<min_size * power_2<1>::value>; break;
case 2: impl_[index] = new Impl<min_size * power_2<2>::value>; break;
case 3: impl_[index] = new Impl<min_size * power_2<3>::value>; break;
case 4: impl_[index] = new Impl<min_size * power_2<4>::value>; break;
}
ret = impl_[index];
}
BOOST_STATIC_ASSERT(POOL_CNT <= 5);
assert(index < POOL_CNT);
return ret;
}
};
간단히 설명하자면 Allocator의 template parameter인 MIN_SIZE와 MAX_SIZE는 16, 256과 같이 2의 자승으로 된 숫자로 MemPool 클래스가 사용할 최소 크기와 최대 크기를 나타냅니다. 이 크기를 가지고 MemPool은 16, 32, 64, 128, 256의 다섯개 크기의 Impl을 사용하게 됩니다. 위의 코드의 get_impl 함수는 index를 가지고 이 크기에 해당하는 Impl 객체를 리턴하는 함수로 아직 해당 Impl 객체가 생성되지 않았다면 new 한 후 리턴해 줍니다. 여기서 문제가 되었던 것은 특정 크기의 Impl을 new하기 위한 switch-case 부분인데 여기를 MIN_SIZE와 MAX_SIZE에 맞게 자동으로 case 구문이 늘어나거나 줄어들도록 만들지 못했었습니다. 그래서 임시 방편으로 BOOST_STATIC_ASSERT로 일단 잘못되면 컴파일 에러가 발생하도록 만들어 놓았었죠. 컴파일 에러가 나면 case를 추가할 수 있도록 말이죠. ;-) 하지만 그때 생각하지 못했던 것은 recursive template을 사용하는 것이었습니다. 아이디어만 떠올랐다면 어렵지 않았을 텐데... 최근에 까막님의 downcast overloading 글을 보고 난 후에야 생각할 수 있었네요. :-| 이 방법을 사용해 수정한 get_impl 함수는 다음과 같습니다. static Base* get_impl(int index) {
const int POOL_CNT = log_2<max_size, min_size>::value + 1;
static Base* impl_[POOL_CNT] = { 0 };
Base* ret = impl_[index];
if (ret == 0) {
ret = impl_[index] = ImplFactory<min_size - 1, pool_cnt>::make(index);
}
assert(index < POOL_CNT);
return ret;
}
그리고 사용된 ImplFactory 클래스는 다음과 같습니다. template <int min_size, int n>
struct ImplFactory
{
static Base* make(int index) {
if (index == N) return new Impl<min_size * power_2<n>::value>;
return ImplFactory<min_size - 1, n>::make(index);
}
};
template <int min_size>
struct ImplFactory<min_size, 0> {
static Base* make(int index) {
return new Impl<min_size>;
}
};
이것으로 MemPool 구현에서 가장 맘에 걸리던 부분이 해결되었네요. log_2나 power_2에서 열심히 썼던 recursive template을 사용해서 함수 호출할 생각을 못했다니... 좀 부끄럽습니다. :-(성능상 기존 방법에 비해 함수 호출에 따른 overhead가 조금 있을 수 있겠지만 매번 호출되는 것이 아니라 각 Impl에 대해 한번씩만 호출되는 것이므로 문제는 없어보입니다. 이런 overhead에 신경쓸 사람도 많지 않겠지만요. :-)
도움이 되셨다니 다행이네요 :)
ReplyDelete함수호출 cost가 걱정되면 inline!!! ㅋ
minsize-1이 이 아니라 n-1일 것같네요. 많이 배우고 있습니다:)
ReplyDelete