王朝网络
分享
 
 
 

Guru of the Week 条款28:“Fast Pimpl”技术

王朝vc·作者佚名  2006-01-08
宽屏版  字体: |||超大  

GotW #28“Fast Pimpl”技术(The "Fast Pimpl" Idiom)

难度:6 / 10

采用一些称为“低依赖度”或“效能”方面的捷径,在很多时候颇有诱惑力,但它不总是好主意。这儿有个很精彩的方法能在客观上同时并安全的实现二者。

问题

标准的malloc()和new()调用的开销都是很大的。在下面的代码中,程序员最初在class Y中设计了一个类型X的成员:

// file y.h

#include "x.h"

class Y {

/*...*/

X x_;

};

// file y.cpp

Y::Y() {}

这个class Y的申明需要class X的申明已经可见(从x.h中)。要避免这个条件,程序员首先试图这么写:

// file y.h

class X;

class Y {

/*...*/

X* px_;

};

// file y.cpp

#include "x.h"

Y::Y() : px_( new X ) {}

Y::~Y() { delete px_; px_ = 0; }

这很好地隐藏了X,但它造成了:当Y被广泛使用时,动态内存分配上的开销降低了性能。最终,我们这无畏的程序员偶然间发现了“完美”解决方案:既不需要在y.h中包含x.h,也不需要动态内存分配(甚至连一个前向申明都不需要!):

// file y.h

class Y {

/*...*/

static const size_t sizeofx = /*some value*/;

char x_[sizeofx];

};

// file y.cpp

#include "x.h"

Y::Y() {

assert( sizeofx >= sizeof(X) );

new (&x_[0]) X;

}

Y::~Y() {

(reinterpret_cast<X*>(&x_[0]))->~X();

}

讨论

解答

简短的答案

不要这样做。底线:C++不直接支持不定类型(opaque types),这是依赖于这一局限上的脆弱尝试(有些人甚至称此为“hack”[注1])。

程序员期望的肯定是其它一些东西,它被称为“Fast Pimpl”方法,我将在“为什么尝试3是糟糕的”一节中介绍。

为什么尝试3是糟糕的

首先,让我们稍许考虑一下为什么上面的尝试3是糟糕的,从以下方面:

1. 对齐。不象从::operator new()得到的动态内存,这个x_的字符buffer并不确保满足类型X的对齐要求。试图

让x_工作得更可靠,程序员需要使用一个“max_align”联合,它的实现如下:

union max_align {

short dummy0;

long dummy1;

double dummy2;

long double dummy3;

void* dummy4;

/*...and pointers to functions, pointers to

member functions, pointers to member data,

pointers to classes, eye of newt, ...*/

};

它需要这样使用:

union {

max_align m;

char x_[sizeofx];

};

这不确保完全可移植,但在实际使用中,已经足够完美了,只在极少的系统(也可能没有)上,它不能如愿工作。

我知道有些老手认可甚至推荐这种技巧。凭良心,我还是称之为hack,并强烈反对它。

2.脆弱。Y的作者必须极度小心处理Y的其它普通函数。例如,Y绝不能使用默认赋值操作,必须禁止它或自己提供一个版本。

写一个安全的Y::operator=()不是很难,我把它留给读者作为习题。记得在这个操作和Y::~Y()中考虑异常安全的需求。当你完成后,我想,你会同意这个方法制造的麻烦比它带来的好处远为严重。

3.维护代价。当sizeof(X)增大到超过sizeofx,程序员必须增大sizeofx。这是一个没有引起注意的维护负担。选择一个较大的sizeofx值可以减轻这个负担,但换来了效能方面的损失(见#4)。

4.低效能的。只要sizeofx大于sizeof(X),空间被浪费了。这个损失可以降到最小,但又造成了维护负担(见#3)。

5.顽固不化。我把它放在最后,但不是最轻:简而言之,明显,程序员试图做些“与众不同”的事情。说实话,在我的经验中,“与众不同”和“hack”几乎是同意词。无论何时,只要见到这种“颠覆”行为--如本例的在字符数组中分配对象,或GotW#23中谈到的用显式的析构加placement new来实现赋值--,你都一定要说“No”。

我就是这个意思。好好反思一下。

更好的解决方法:“Fast Pimpl”

隐藏X的动机是避免Y的用户必须要知道(于是也就依赖)X。C++社区中,为了消除这种实现依赖,通常使用“pimpl”方法(注2),也就是我们这个无畏的程序员最初所尝试的方法。

唯一的问题是,“pimpl”方法由于为X的对象在自由空间分配内存而导致性能下降。通常,对特殊类的内存分配性能问题,采用为它提供一个operator new的重载版本的方法,因为固定尺寸的内存分配器可以比通用内存分配器性能高得多。

不幸的是,这也意味着Y的作者必须也是X的作者。通常,这不成立。真正的解决方法是使用一个高效的Pimpl,也就是,有自己的类属operator new的pimpl类。

// file y.h

class YImpl;

class Y {

/*...*/

YImpl* pimpl_;

};

// file y.cpp

#include "x.h"

struct YImpl { // yes, 'struct' is allowed :-)

/*...private stuff here...*/

void* operator new( size_t ) { /*...*/ }

void operator delete( void* ) { /*...*/ }

};

Y::Y() : pimpl_( new YImpl ) {}

Y::~Y() { delete pimpl_; pimpl_ = 0; }

“啊!”你叫道,“我们找到了圣杯--Fast Pimpl!”。好,是的,但先等一下,先想一下它是如何工作的,并且它的代价是什么。

你所收藏的C++宝典中肯定展示了如何实现一个高效的固定尺寸内存分配/释放函数,所以我就不多罗嗦了。我要讨论的是“可用性”:一个技巧,将它们放入一个通用固定尺寸内存分配器模板中的技巧,如下:

template<size_t S>

class FixedAllocator {

public:

void* Allocate( /*requested size is always S*/ );

void Deallocate( void* );

private:

/*...implemented using statics?...*/

};

因为它的私有细节很可能使用static来实现,于是,问题是Deallocate()是否被一个static对象的析构函数调用(Because the private details are likely to use statics, however, there could be problems if Deallocate() is ever called from a static object's dtor)。也许更安全的方法是使用single模式,为每个被请求的尺寸使用一个独立链表,并用一个唯一对象来管理所有这些链表(或者,作为效能上的折中,为每个尺寸“桶”使用一个链表;如,一个链表管理大小在0~8的内存块,另一个链表表管理大小在9~16的内存块,等等。):

class FixedAllocator {

public:

static FixedAllocator* Instance();

void* Allocate( size_t );

void Deallocate( void* );

private:

/*...singleton implementation, typically

with easier-to-manage statics than

the templated alternative above...*/

};

让我们用一个辅助基类来封装这些调用:

struct FastPimpl {

void* operator new( size_t s ) {

return FixedAllocator::Instance()->Allocate(s);

}

void operator delete( void* p ) {

FixedAllocator::Instance()->Deallocate(p);

}

};

现在,你可以很容易地写出你任意的Fast Pimpl:

// Want this one to be a Fast Pimpl?

// Easy, then just inherit...

struct YImpl : FastPimpl {

/*...private stuff here...*/

};

但,小心!

这虽然很好,但也别乱用Fast Pimpl。你得到了最佳的内存分配速度,但被望了代价:维护这些独立的链表将导致空间效能的下降,因为这比通常情况现成更多的内存碎片(Managing separate free lists for objects of specific sizes usually means incurring a space efficiency penalty because any free space is fragmented (more than usual) across several lists)。

和其它性能优化方法一样,只有在使用了profiler剖析性能并证明需要性能优化后,才使用这个方法。

(注1. 我不是其中之一。 :-))

(注2. 参见GotW #24.)

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
>>返回首页<<
推荐阅读
 
 
频道精选
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有