« opera fans | 返回首页 | 网络游戏中的货币系统 »

对象和资源的管理

用了这么长时间的 C++ 架构软件,最头痛的莫过于管理内存中的对象和资源。而在管理对象方面,最难处理的就是删除对象的时机。恐怕很多人早就意识到这一点,所以才有了 gc 技术的蓬勃发展。

当一个对象被很多地方引用的时候,通常我们会给出引用记数,当记数减到 0 的时候就删除,这是个看似完美的解决方案。但是,有多少地方会记得解除引用呢?借助 C++ 的语法糖,可以自动的完成这些工作。长期的引用关系,可以在构造和析构的时候操作;短期的引用,比如就在一个函数内获得对象,操作完毕后马上解除引用。这个时候,可以通过返回几个 warpper 对象来完成。

固然,语法糖可以减少出错的可能,但是,代码还是并隐式的加上了,于之伴随的是运行时性能的下降和代码的膨胀。这个时候,多考虑下设计上的改进会有所收获。

最近两年做的项目,我都趋向于对象统一做删除管理。可以在主线程中安排一个定期时间,扫描所有对象,把需要删除的对象删除。而平时的删除,只是做一个简单的 mark 操作。因为 mark 操作是不可逆的,即不能把一个准备删除的对象 mark 回"不必删除"的状态,所以 mark 操作本身是线程安全,不必加锁的。

为什么这样做?因为对于单种对象或是资源而言,大部分情况都是只读的,很多东西经过初始化后,不再有写操作。即使有,也是 os 保证线程安全的。例如文件 handle , socket 这些。而每个对象都会有一种特殊的操作叫做释放,释放可以看成一种写操作,它导致了对象本身的破坏。当一个对象是完全只读的时候,我们就不需要再考虑它的线程安全问题,就是因为有释放这种操作的存在,破坏了完全只读的特性。当我们把释放提取出来统一放在安全的地方处理的时候,我们就可以让大多数对象完全只读,而不用估计线程安全了。

而且这种做法在单线程环境也是有意义的,用一些简单的技巧(例如用一个指针间接访问对象),甚至不用再使用引用计数。

对于单件的处理,采用静态对象和惰性初始化的方案,简直就是 C++ 程序员的陋习。Double Checked Locking is broken 相信很多人都读过了。过于依赖语法糖,通常就会造成这种结果。其实让程序有明显的初始化和退出阶段,是很容易被规划出来的。把单件(singleton) 的处理放在正确的时机,以正确的次序来处理并非难事。

对于全局所有资源的管理,我个人的主张是一定要考虑采用树结构,而不是线性表。因为扫描所有资源这种操作会比较常见。比如以上提到的扫描所有资源,删除 mark 过的对象。采用树结构的好处是,mark 的时候,可以同时 mark 父节点(对父节点计数)。这样,任何资源树上的任何一支都可以通过 root 知道是否需要遍历分支。通常,删除这种操作并不频繁,通过检查根节点一次就可以忽略整个遍历过程了。

而且删除操作往往是可以并行的。为了在删除过程不影响资源树的结构,我们还可以只是对资源树上的节点置空,再统一压缩掉空指针。这样就可以获得最大效率的删除操作,不至于因为定期删除资源而使服务停顿过久。

Comments

这证明,完全不使用全局变量和静态变量是可以做到的。

这一年以来想法改变了许多,从去年8月到现在,我居然再也没有写过任何使用单件的代码,我所有的代码都避免了单件。所有的“全局”作用的对象,全部都延迟到main函数里面,作为main函数的局部变量来做。

今天碰到一处依赖于别人的代码的,而他使用了单件的。不得不说的是,单件具有传染性,我也就不得不使用了单件……结果引发了很多问题,还好那个代码是Java的,考虑使用给调用单件的代码指定不同的ClassLoader来让其依赖的单件能够具有多个。这种技巧很丑陋,就好像Windows里面把依赖于单件的DLL复制多个不同
的命名,然后加载多次以便使用多个实例一样……

对于单件的处理,采用静态对象和惰性初始化的方案,简直就是 C++ 程序员的陋习。

------------
这的确是C++程序员的陋习,不过,我突然想到,这并不是.net或java程序员的陋习。.net或java中的静态对象的初始化由框架来决定初始化顺序,没有被访问的对象貌似还有延时加载的情况。

似乎C++的静态对象的初始化顺序时编译是决定的,而.net或java则是运行时决定的。

如果说语法糖的话,如果这个语法糖做的足够完善以至于不出问题,为什么不用呢?更何况静态对象的初始化放在相关的类中的确更符合面向对象的代码组织方式

原子加和减(InterlockedIncrement linux内核里对应的东西叫atomic_inc atomic_dec)一般来说比线程锁快 比+ - 慢 不过也有更慢的时候 比如在arm下 armv6以前的指令集是没有内存总线锁的 幸好也没有smp 所以内核里原子操作可以通过关中断实现 在用户级 就得用线程锁 所以原子操作和加/解锁一次代价是一样的 做掌机游戏不可不察 不过相对于内存拷贝的开销来说 这些都小case了

to Detective :
你做的就是内存池。

大家好! 现在c++的内存管理,有2套思路:一是Java,.net和COM等现代语言技术普遍使用的GC;另一个是智能指针。
我感觉,GC机制,在Java等语言的实践中,效果非常的好。Java程序员可以完全不考虑堆内存的释放问题。但是,在C++阵营中,支持使用GC的人还是比较少,特别是国内的C++社群。
智能指针,在C++中的拥护者甚众,包括一些国外的著名类库,如ICE也是使用智能指针技术。这批支持者,大抵是认为C++有析构函数,不用可惜。我对智能指针技术,还没有太深入的认识,无法对此予以评论。不过,没有析构函数,对于Java来说,并不是一个问题。Java中可以使用try{}catch(){}finally{}的机制肯定的释放资源!
另外,实际上,Java中要实现析构函数这样的机制,也并不是毫无办法。相信可以藉由AOP实现同样的效果。

我做服务器的经验,可以不进行垃圾回收,所有的对象删除后就放进freelist,这样可以更快的New出来。

至于内存池,并不是必要的。

还有Id的问题,我一般都是使用32位Id,低16位作为数组索引,高16位作为简易时间标签,这样,基本能够保证短时间内没有同一个id可以被重复使用。

很久以前,我使用了一种称为IndexList的容器来实现资源和对象管理,它是自动化分配对象内存,以及分配对象Id。但是,有个缺点是大小一开始限制死了。不过有个优点是所有操作都不会有循环,绝对性能第一。
并且,可以做遍历操作。
First以下,然后Next就行了。

下面是代码,CLock是自动锁定和解锁临界区的一个辅助类,没什么技术含量。


代码开始(年代久远,所以写的有些乱,请多见谅):
template < class T , int MAXCOUNT>
class CIndexList
{
typedef struct _node_
{
T * data;
_node_ * pnext;
_node_ * pprev;
unsigned int nextfree;

}st_node;
BOOL _Clean()
{
int i = 0;

for( i = 0;i <= MAXCOUNT;i ++ )
{
m_pArray[i].data = NULL;
m_pArray[i].pnext = NULL;
m_pArray[i].pprev = NULL;
m_pArray[i].nextfree = i + 1;
}
m_pArray[MAXCOUNT].data = NULL;
m_pArray[MAXCOUNT].nextfree = 0;
m_pArray[MAXCOUNT].pnext = NULL;
m_pArray[MAXCOUNT].pprev = NULL;
m_free = 1;
m_pHead = &m_pArray[0];
m_pTail = m_pHead;
m_pThrough = m_pHead;
m_totel = 0;
m_bLocked = FALSE;
return TRUE;
}
public:
CIndexList()
{
//int i = 0;
m_pArray = NULL;
//if( MAXCOUNT == 7000 )
// GetTickCount();
m_pArray = new st_node[MAXCOUNT + 1];

_Clean();
}
VOID Clean()
{
int i = 0;

for( i = 0;i <= MAXCOUNT;i ++ )
{
//m_pArray[i].data = NULL;
m_pArray[i].pnext = NULL;
m_pArray[i].pprev = NULL;
m_pArray[i].nextfree = i + 1;
}
//m_pArray[MAXCOUNT].data = NULL;
m_pArray[MAXCOUNT].nextfree = 0;
m_pArray[MAXCOUNT].pnext = NULL;
m_pArray[MAXCOUNT].pprev = NULL;
m_free = 1;
m_pHead = &m_pArray[0];
m_pTail = m_pHead;
m_pThrough = m_pHead;
m_totel = 0;
m_bLocked = FALSE;

}
virtual ~CIndexList()
{
// CLock m_lock(&m_CriticalSection);
int i = 0;
for( i = 0;i < MAXCOUNT;i ++ )
{
if( m_pArray[i].data != NULL )
{
delete m_pArray[i].data;
m_pArray[i].data = NULL;
}
}
if( m_pArray != NULL )
delete []m_pArray;
}

public:
unsigned int GetCount()
{
//CLock m_lock(&m_CriticalSection);
return m_totel;
}
int Reset()
{
CLock m_lock(&m_CriticalSection);
m_pThrough = m_pHead;
return 1;
}
VOID Lock()
{
m_CriticalSection.Lock();
}
VOID UnLock()
{
m_CriticalSection.Unlock();
}
T * First()
{
// CLock m_lock(&m_CriticalSection);
//if( !m_bLocked )
// return NULL;
if( m_pHead == NULL )
return NULL;
m_pThrough = m_pHead->pnext;
if( m_pThrough != NULL )
return m_pThrough->data;
return NULL;
}
T * Cur()
{
// CLock m_lock(&m_CriticalSection);
//if( !m_bLocked )
// return NULL:
if( m_pThrough != NULL && m_pThrough != m_pHead )
return m_pThrough->data;
return NULL;
}
T * Next()
{
// CLock m_lock(&m_CriticalSection);
//if( !m_bLocked )
// return NULL;
if( m_pThrough != NULL )
m_pThrough = m_pThrough->pnext;
if( m_pThrough != NULL )
return m_pThrough->data;
return NULL;
}
T * End()
{
// CLock m_lock(&m_CriticalSection);
//if( !m_bLocked )
// return NULL;
if( m_pTail != NULL )
return m_pTail->data;
return NULL;
}
unsigned int New( T ** t )
{
CLock m_lock(&m_CriticalSection);
unsigned int id = 0;
id = AllocId();
if( id == 0 || id > MAXCOUNT )
return 0;
if( m_pArray[id].data == NULL )
m_pArray[id].data = new T;

*t = m_pArray[id].data;
m_pTail->pnext = &m_pArray[id];
m_pArray[id].pprev = m_pTail;
m_pArray[id].pnext = NULL;
m_pTail = &m_pArray[id];
m_totel ++;
return id;
}
int Del( unsigned int id )
{
CLock m_lock(&m_CriticalSection);
if( id > MAXCOUNT || id == 0 )
return 0;
if( m_pArray[id].pprev == NULL && m_pArray[id].pnext == NULL )
return 0;
if( m_pArray[id].pprev != NULL )
{
if( m_pThrough == &m_pArray[id] )
m_pThrough = m_pArray[id].pprev;
m_pArray[id].pprev->pnext = m_pArray[id].pnext;
}
else
return 0;
if( m_pArray[id].pnext != NULL )
m_pArray[id].pnext->pprev = m_pArray[id].pprev;
else
m_pTail = m_pTail->pprev;

//if( m_pTail == &m_pArray[id] )
//{
// m_pTail = m_pTail->pprev;
//}

m_pArray[id].pprev = NULL;
m_pArray[id].pnext = NULL;
ResaveId( id );
m_totel --;
return 1;
}
T * Get( unsigned int id )
{

CLock m_lock(&m_CriticalSection);
if( id == 0 )
return NULL;
if( id <= MAXCOUNT )
{
if( m_pArray[id].pnext == NULL && m_pArray[id].pprev == NULL )
return NULL;
return m_pArray[id].data;
}
return NULL;
}
private:
unsigned int AllocId()
{
CLock m_lock(&m_CriticalSection);
unsigned int ret = m_free;
if( ret != 0 )
m_free = m_pArray[ret].nextfree;
return ret;
}
int ResaveId( unsigned int id )
{
CLock m_lock(&m_CriticalSection);
if( id > MAXCOUNT || id == 0 )
return 0;
m_pArray[id].nextfree = m_free;
m_free = id;
return 1;
}
private:
BOOL m_bLocked;
CriticalSection m_CriticalSection;
unsigned int m_free;
unsigned int m_totel;
st_node * m_pArray;
st_node * m_pHead;
st_node * m_pThrough;
st_node * m_pTail;
};

用法
初始化
CIndexList<Object,2000> objlist;
创建时
Object * pObj = NULL;
UINT id = objlist.New( &pObj );
pObj->SetID( id );

删除时
UINT id = pObj->GetID();
objlist.Del( id );

访问时
Object * pObj = objlist.Get( id );

我说的正是运行期不固定这个 id . 你说的 "在运行期如果对于得id对象变化的话会带来很多问题" 是存在的也是可以避免或者被解决的. 如果存在"太多问题" 那应该是设计问题. 我们只需要保证一个对象的 id 在生命期不变就可以了. 整个世界中活着的对象并不多.

To cloud:
理解错我的意思了,你所说的 id 并不是一个 object 的唯一 id,只是个idxId,即在服务器运行期这个idxId对应的对象并不唯一。在运行期如果对于得id对象变化的话会带来很多问题。

其实道理很简单,15bit 不够可以用 16bit, 17bit ... 只要是临时分配出来即可。
因为相对应一个 lookup table 来说,无非给每个对象多一个 4 字节的映射空间。网络包在 client 和 server 间交换信息用这个临时 id 即可。这样做其实是把从临时 id 到永久 id 转换的负担一部分转嫁给 client 了。

你的对象总会在内存中放着的,多一张表只是每个对象增加 4 字节的空间了。当然有时候要做删除增加的管理。每个对象需要 8 字节来处理。

To Cloud:
恩,player 可以这么做,并且我这边也是类似这么做的

但是 role 里还有 monster, npc,pet 等等, 15bit 是不够的,并且不同的类型,有不同的 id 段,统一用 rolemanager 管理了,太大了,没有办法放在一个数组里,这个有什么良好的方案没有?

to KxjIron :
我也云风的办法差不多,用数组结合内存池的办法来管理Id.这样减少了查表时间。

我的 id 解决方案是,每个player 在线会被分配一个 15bit 的临时 id ,下线就失效。对于 15bit 的 id 到 player 的映射,用一个数组即可。

To sunway:
我得出的结果和你一样
通过 id 查 role 消耗了很多 cpu。这也令我很惊奇,以至于一些重要的地方我改成用自动维护的指针代替了通过 id 访问的方式。

我们的 RoleManager() 是通过 SGI 的 hash_map 实现的,为什么会慢,我没有去试验,是否是因为 cache 失效导致的?

你是怎么解决的呢?

好不精彩啊!
感觉很简单的一个问题,被讨论到了InterlockedIncrement的内部实现和效率上了。
前面看这篇blog时,也是想说点啥,不过后来作罢了。因为,资源和内存管理是必要的,当然,我想这里主要讨论的是这个管理策略是否适宜。——策略是每个实现者的事。^_^
是看到了Atry关于“严于律己”的论述,让我长久的苦闷顿时愉悦起来,突然想上来冒个泡。

最内层的循环自然是最消耗 cpu 的地方,只是从表面有时候很难判断哪里才是。如果代处理的事务的层次结构是一颗树,的话,最底层的叶子也就最多。从层次结构上讲,这些叶子都是同级的,但并非最底/内 层就一定是效率瓶颈。网络 buffer 的处理和图象的处理都可以看做是最底层,但大多数情况下, client 的网络 buffer 处理都不应该是效率问题所在。我实际碰到了,但这是经过实践才知道的,一开始的设计阶段就想不到由于 C 到 python 的转变会导致效率问题显露出来,在底层的其他模块,诸如底层 UI 消息处理上,我们同样用的 python 就没有问题。

analyst, 当然该传原生指针的地方传原生指针, 这一点不正说明,有时候 smartptr 会成为效率影响因素吗?从更纯粹的设计角度讲,原生指针是 C 的东西,而不是 C++ 的。我说的正是这个问题,并非 C++ 提供了的东西我们就应该去用。其实我不想做口舌之争,你也应该知道我并非反对用 smartptr ,这里想说明的只是,没错,当我们每个地方都严格的使用 C++ 已经提供了我们的机制,比如每个指针都用 smart_ptr ,就永远不用担心对象被销毁掉,又被继续使用的情况。但是,实际情况中,我们往往会因为效率方面的考虑而不这样做,同时也认为不这样做同样也是安全的。但这种安全性建立在我们对整个系统的设计的理解上,并非依赖语言本身提供的机能。

而且,当操作是异步的时候,传原生指针并不安全,很可能处理指针的时候它已经被销毁掉了。从安全角度讲,还是传 smart_ptr 比较好。可当我们知道对象的生命期时就不一样。我上面 blog 说的方案,也是为了更明确的知道对象销毁的时间。它不是唯一的解决方案,只是一个可行的方案而已。

你也说了,

就网络数据处理而言,client 无非就是一秒几K的数据量。由于我们新算法中,不再放明文在内存,导致处理数据量加大,python 虚拟机不堪重负,这是开始没有想到的。

让python处理了网络底层的缓冲,这就是最内层循环,当然会影响效率

这正好符合我的观点

好像有一句话是说CPU 99%的时间都在运行最内层的循环。
server的主要开销应该是IO吧?

根据我的经验,对于server而言,有Gate的server的CPU时间消耗在查表(比如根据ID查用户对象)和碰撞检测(简单的走格子的话这个开销不大)上,对于无Gate的server可能还要消耗部分CPU时间来处理大量socket上的I/O。如果有脚本的花还要消耗在脚本上。剩下的逻辑代码无非是一些判断和加法减法,只要不是太烂,消耗不了多少CPU时间。

to sunway:盲目的反对是没有意义的,要拿出证据来。

to 云风:在多线程的程序中也没有必要让smart_ptr都用InterlockedIncrement来改变引用计数的,不是所有对象都需要多线程访问的,对于单线程对象来说完全可以直接加减。实际情况是,即使多线程程序也要十分谨慎使用多线程对象,否则你的代码很容易就失控了。
传递一个对象给函数,在函数入口和出口处各加减一次引用是没有意义的操作,完全可以传递一个对象的原生指针,避免无谓的加减。

游戏的 client 为什么绘图的代码容易产生效率瓶颈?因为相对于系统需要管理的对象数目,一个对象上需要处理的像素的数目是可以多出一个数量级的。但是,当需要处理的像素不那么多的时候,(比如我们对 2d engine 做了优化,剔除掉静止的对象的绘制,或者使用显卡的 3d 特性,以贴图和多边形为处理单位)逻辑对象的数量就和像素/多边形的数量相差不那么多。这个时候,瓶颈就会部分转移。这种事情我亲生经历过。那就是有一次西游系列的两个产品更换网络层处理模块。除了更换算法,我们更是将所有算法用脚本重写。大话西游用的 lua ,梦幻西游用的 python 。

结果是,新版本发布后,大话的 client 玩家是可以接受的,但是梦幻的 client 很多玩家抱怨说太卡。经过检查,发现效率瓶颈发生在新增加的 python 模块内。并不是我们 python 代码写的不好,的的确确就是 python 自生的效率问题导致的。

就网络数据处理而言,client 无非就是一秒几K的数据量。由于我们新算法中,不再放明文在内存,导致处理数据量加大,python 虚拟机不堪重负,这是开始没有想到的。

对于 server 而言,由于没有的图形处理,一个显而易见的大数据量事务没有了,到底什么会成为效率瓶颈,还是要留给实测了。

上百时钟周期不多,但是多不多往往看的是相对值。效率符合要求也不等于效率高。更好的方案总是存在,但是大多数情况不会去用正是因为效率符合要求。

只要是多线程的程序,smart_ptr 在实现的时候,对引用的操作都应该用 InterlockedIncrement 。

在没有具体应用的情况下,我们讨论方案,由于没有具体性能指标的要求,所以不能认为上百时钟周期到底是多还是不多。没有十全十美的解决方案,所以这篇 blog 也只是提出针对一类问题的解决方案而已。

我个人很反对用引用计数的,在多线程下效率很一般,完全可以用其他办法替换掉。

我实际测试过InterlockedIncrement 和临界区的开销,在没有竞争情况下和无竞争情况下InterlockedIncrement 的效率都比临界区高一些。

上百时钟周期很多吗?

需要InterlockedIncrement的是什么情况?

只要不是最内层的CPU密集运算循环吗,完全不用考虑这种底层实现的开销。

在上层,只需要考虑调用耗时操作(如图形底层/IO)的方式有没有问题,上层代码本身的逻辑开销完全可以忽略。
比如,对于上层代码来说,如果是加锁用得不好,导致多个线程在等待一个耗时线程,那么是问题。

但是InterlockedIncrement绝对不是问题,现在都是G级别的CPU,引用计数那点开销算个啥哦。而我想,有大脑的人绝对不会在最里层的一层计算alpha混合之类的代码里面还要考虑引用计数吧?

C/C++程序员很容易被误导,看多了《高质量C/C++编程》之类的书就容易吹毛求疵,当然,我明白《高质量C/C++编程》说的是正确的,很多地方也很有用,但是除了最内层的密集CPU运算的循环需要考虑生成什么汇编码,上层的统统都不用考虑,用java或者C#来做决不会比C/C++慢,甚至因为java或者C#的语法在某些地方不容易犯错,对于新手来说,写出来的java/C#代码往往比C/C++更快。

甚至我觉得,对于网络游戏来说,不管是客户端还是服务器端都可以用java/C#来写,一个上G的网络游戏多一个几十M的JRE或者.net framework有什么关系。C/C++/ASM唯一要做的就是图形底层写几行最内层循环就够了。

顺便说点别的,包装这种东西,理论上大家都知道要“严于律己,宽于待人”。所谓“严于律己”就是对于别人写的接口一定要严格搞清楚底层实现,“宽于待人”就是说自己封装给别人的接口要让别人不必知道底层实现。但实际上我觉得底层代码写多了的人往往不自觉就会把“严于律己”推广到“‘严’于待人”。我接触了很多java的第三方库之后才对比发现C++的很多库的封装就是“‘严’于待人”。

我写这些话不是针对谁对谁错,只是觉得InterlockedIncrement这样的函数完全不用讨论其开销,对于上层代码来说,闭着眼睛用就行了。如果是为了学习,“严于律己”固然没错,但是要节省时做事,不需要考虑的东西就可以不需要考虑。

LOCK 指令是要锁总线的,越先进的 cpu 上越慢,多核的时候问题更大。花上上百时钟周期并不奇怪。

如果一个函数入口和出口用这个加减一次引用,很有可能比整个函数体运行时间都长。我们不能看绝对时间,要看相对时间。这些影响流水线的东西,如果有方法避免当然更好。

引用记数固然在大多数情况会比值拷贝来的高效,但若它真的永远高效,就不会有人反对用 cow 的 string 解决方案了。

InterlockedIncrement是CPU提供的原子操作,这个锁定的开销跟操作系统锁的开销不可同日而语。在没有锁冲突的时候性能跟直接内存操作是一个数量级的,有冲突的时候(这个应该在多CPU系统上才可能发生,没法测试)我猜测开销跟一次cache失效差不多,而且冲突几率是极低的。看来讨论一下也有好处,我特地查阅了一下InterlockedIncrement在IA32上是通过LOCK XADD dword ptr [ecx], eax来实现的。


PS,我只是想指出一些偏见(诸如引用计数低效无能)。具体哪个方案好当然是跟具体应用相关的,这个不是我们讨论的目的。

其实前面的帖子有人说的挺好,原话不是逐字这样说的:

C++ 天生是为了做大项目用而设计出来的,但是世界上已存的诸多大的项目却是用 C 来实现。而应用级的的大项目却用 java 实现。

至于效率,我在公开场合多次表示过自己的观点,仅仅是从语言和编译器实现角度看。C++ 往往比 C 做的高效。

语言之争是没有意义的,把 C++ 的发展历程看做是汇编到C再到C++ 的进化只是井蛙之见。C++ 是和 C 不同的语言,从 C 那借鉴来的仅仅是语法和名字而已。

to analyst, 心理上觉得原子操作InterlockedIncrement 没有效率问题是不行的,这需要实测。你可以实测一下。至于 InterlockedIncrement 指令本身,也是上了锁的,只是这个锁是隐式的罢了。

我这里说的是设计问题,并非语句的效率问题。至于回收垃圾的问题,我们可以想办法从设计上实现并行,并行是多核时代发展的趋势。

我在这里也只是提供一种方案,至于它是否有合适的场合,应该由考虑具体应用的人来做。绝对正确和普适的方案是不存在的。

ps. 每次和 analyst 讨论问题,我都有种不祥的预感 :D

引用计数可以通过原子操作InterlockedIncrement来改变,不需要加锁哦。
云风似乎是过高的估计了引用计数的代价,但是不要让心理上的担忧代替实际的测试。统一扫描和删除对象也不是没有性能问题,这可能会带来运行时的颠簸,对于某些应用中颠簸比降低平均运行性能更不可接受。

片面追求效率是低智商的表现。幸亏还有高智商的人存在,否则我们就一直要用机器码编程了。还有,你在主页好几个地方说过为什么不用C++而用C,其理由不像是一个程序员在说话,更像是李洪志大师在告诉信众为什么要用法轮功而不是医学来治疗疾病。

to NoSound, 你没有理解这篇文章。或者说你没有理解其要点。

引用记数是可以做的,我们可以在引用记数到 0 的时候做 mark 而不是 delete 。

这里的要点在于 mark 操作行为可以不加锁的同时操作,不会破坏对象的只读性。而加减引用则必须加锁。而且依赖引用去删除对象,有可能导致对象析构。

其结果是,若严格按引用记数的方法管理对象的生存期,那么即使是读对象也需要加一次引用。(否则无法保证读对象的期间对象不被另外线程销毁掉)以上提到的方案的重点之一在于解决这个问题。(同时我认为其它的好处也有很多)

而如果删除对象只是 mark 的话,对象即使逻辑上被删除了,它也是可读的。

因为大多数的应用的服务都是周期性的。我们只需要保证在一个服务周期内对象存在或不存在即可。在服务周期的空挡去删除对象是一个合理的时机。

如果真的需要对象引用计数,那么只需要在长期稳定的引用关系中做即可。这一般发生在相互依赖的对象构造和析构的时刻。如果所有对象的析构都集中在一起,即,无所谓析构的次序时,处理起来也便捷的多。滥用 smart pointer 这样的东西,经常也会造成效率问题,比如用 std::vector 存放智能指针。

因为一般对象析构的最后一步,即系统资源的释放,(包括真正占用的内存,文件,socket 等),是相互没有依赖关系的。比如,我们可以顺着调用所有的析构函数,再集中回收占用的系统资源等。这样往往可以更为快捷的完成,甚至做到并行。

C++ 的确明显的提供了我们一条正确且方便的解决问题的思路和方案(如对象的生存期管理,smart pointer 这样的便捷手段)。但正确的思路不是唯一的思路,也不一定是最佳的思路。

ps. gc 同样如此,gc 解决的是内存管理的问题,它并没有解决资源管理的问题。真的遇到 "我有一个Socket对象,我可以保证在析构的时候就Close这个套接字,但在C#里,很痛苦地不能这么做,因为鬼才知道这个对象什么时候才销毁
" 这个情况的时候,为何不使用引用记数?

首先,C++不是C#不是Java,如果非要在C++里专门开一个线程来搞GC,那不如不用C++。C++程序员可以精确控制对象释放的时机,这正是C++的长处,比方说,我有一个Socket对象,我可以保证在析构的时候就Close这个套接字,但在C#里,很痛苦地不能这么做,因为鬼才知道这个对象什么时候才销毁。

第二,怕忘记减少引用计数吗?文中自己也承认C++的语法可以完美地让你在引用一个对象时,自动递增计数,而在不需要它的时候递减,即然有这么好的语法,为什么不用呢?难道编译器会比程序员(人)更易犯错误吗?

第三,原文中说“可以在主线程中安排一个定期时间,扫描所有对象,把需要删除的对象删除。而平时的删除,只是做一个简单的 mark 操作”,那么,谁来做这个mark操作,比方说,我在一个stl容器中放了一个对象的指针,在该stl容器被销毁的时候,它敢mark吗?它如何知道没有人在引用这个对象呢?这个mark和delete又有什么区别呢?

第四,原文中似乎没有地方能证明出“靠编译器(自动)生成的代码来保证引用计数正确”这种做法的不合理性,仅仅是说编译器隐式地生成了一些代码(目标代码多了)。这个是理由吗?现代语言要做的就是让编译器完成更多的事情,让程序员少犯错。难道相比之下,我们宁原愿意让源代码量增加(且易出错)吗?想起一份统计文档,说Perl的一行代码相当于6行C代码所能干的事情,而一行C++代码的大约相当于2.5行C代码,也就是说,越是高级的语言,它的源码量就越少,这是天经地义的,至少说他会生成多少目标代码,这个不应该是考虑的事情吧,而且C++生成的代码的效率很接近C,根本不会因为有了构造和析构就让我们的服务程序动弹不得。

第五,用一个线程来定期扫描所有对象,无论从空间还是时间上都是浪费,如果一个对象自己能记住自己被引用了多少次,它就没有必要让另一个管理器来管理它。

学长有时间的话可以回母校的BBs看一下哈

http://bbs.csu.edu.cn

我想,syntactic 表达的是一句话的构成方式的意思,翻译成句法更好一些。不过这个词不如语法容易被接受。在我的交流圈子中,大家口头上都称呼为语法糖了。

技术术语很难做到信达雅,一般都是大家先了解了一个概念,再附会一个词上去。想从中文词望文生义还是满难的。不过"语法糖"这个词还不算太差。

http://www.google.cn/search?hl=zh-CN&q=syntactic+sugar&btnG=Google+%E6%90%9C%E7%B4%A2&meta=

约有502,000项符合syntactic sugar的查询结果

“语法糖”是正式翻译吗?我怎么觉得应该叫“格式糖”,因为语法貌似更接近“grammar”。现代汉语太不精确,:)。

另外内存管理的话,region-based memory management是比较合适一些批量删除场合的吧,有的地方也叫管这个叫arena或pool,<A href="http://apache.hpi.uni-potsdam.de/document/4_6Memory_resource.html">Apache里面用了这个</a>。

貌似最近出现频率最高的一个新词就是:语法糖...syntactic sugar....
<br>
orz

&#20320;&#25351;&#30340;&#35821;&#27861;&#31958;&#26159;&#25351;&#30340;smart pointer &#20040;?

Post a comment

非这个主题相关的留言请到:留言本