NOTE: Patrick Waytt, from blizard, worked on StarCraft, has write some article on the use of Avoiding game crashes related to linked lists. And the CSDN writer has translated his article to chinese, I am reposting the article here.
The Chinese translation is available here: StarCraft开发:如何避免链表引起的游戏崩溃
如果你是一个使用STL std::list的C++程序员,那么你肯定做错了。但是,只要使用了本文提到的技术,相信你也能跻身精英程序员俱乐部,比同等级的程序员写出更好的代码!
有点垃圾广告的意思?我并不这么认为。根据观察StarCraft和Guild Wars开发人员的成败经历,我开始相信一些没有被广泛接受的的优秀编程实践确实能够对产品质量产生深远影响,下面将给你证明。
我正在看一篇Martin Sústrik的文章,他是ZeroMQ(一个我非常推荐的网络编程库)的首席开发工程师之一。ZeroMQ主要用来处理服务器间的网络交互,包括Mongrel2等优秀作品都在使用它。
Martin最近写了一篇文章来解释他为什么使用C而不是C++来开发ZeroMQ,这也是我为什么写这系列文章的原因。他在第二篇文章里主要说C++标准模板库中的链表是多么的失败(STL std::list library),所以用C语言能够做得更好。
虽然我非常尊敬他在ZeroMQ上的工作,但我认为下面这个方法比用C重新构架这些库更好。
下面是一个使用STL声明链表的方式(代码来自上面提到的文章):
在向上述链表中添加一些成员后,内存中会出现如下内容:
一个使用 C++ std::list创建的双向链表
下面是从链表中安全删除一个person记录的代码:
不幸的是,这里的解链代码非常糟糕:对于一个N条记录的列表,平均需要扫描N/2次才能找到我们想要删除的元素,这也就解释了为什么链表不适合存储需要经常随机访问的数据。
更重要的是,还需要写上面那样删除列表元素的函数,这不仅会占用程序员开发时间、降低编译速度,也更容易出现bug。但如果有一个优秀的链表库,上面的代码就毫无必要了。
下面我们来说说使用intrusive list代替std::list的方案。
intrusive list要求链接域直接嵌入被链接的结构体中。和外部链接表(一个单独的用来保存对象以及前后列表指针的对象)不同的是,intrusive list在结构体被链接时就已经公开其存在了。
以下是上例使用链表重新实现后的代码:
我使用了#define宏来避免代码重复和拼写错误,所以列表的声明代码就成了这样:
如果你看一下内存布局会发现,和std::list相比内存中分配的对象更少了:
一个双向链接的intrusive list(doubly-linked intrusive list)
此外,删除列表中的元素变得更加简单和快速,而且还不需要内存回收:
更好的是,如果你删除intrusive list中的一条记录,它会自动将自己从列表中解链:
做到这一步你可能会疑惑,为什么很多情况下intrusive list比外部链表更好?下面是我所想到的原因:
因为这个被用来包含一个带链表的对象的链接字段通常是嵌入在对象中的,所以它不需要为了将项目链接到列表中而分配内存,也不用在解链的时候回收内存。所以应用速度++、内存占用-。
当遍历存储在侵入式链表中的对象时,它只需要一个指针间接取值就可以获取到对象;相比于std::list使用两个指针间接取值,内存缓存超负荷的情况发生得更少,所以程序也就运行地更快了——特别是在存储器停顿非常之长的现代处理器上。所以应用速度++,缓存压力-。
我们减少了代码故障路径,因为在链接项目时没有必要处理内存外的异常。代码对象大小--,代码复杂度-。
最重要的是,对象会自动从它们链入的列表中删除,消除了很多常见的bug。应用可靠性++。
intrusive list的最大好处在于,它能立竿见影。下面来看看这样实现的原理:
相当漂亮,是不是?使用自动化清理能够避免很多常见错误。
有的人可能会在意包含intrusive list域的对象,这里的intrusive list域跟对象应该包含的数据没有任何关系。person记录被外部的“stuff”“玷污”了。
这样的话,类似直接向磁盘写入记录(不能安全地写指针)、使用memcmp来比较对象(又是这些讨厌的指针)这样的底层功能( leet-programmer stuff)做起来就更难了。其实你完全不应该做这些事,可靠性比速度重要得多!如果你减少使用这些用来提升速度的hack技巧,你的程序会变得更好。不要忘了千年虫bug!
在使用intrusive list时,程序必须声明在声明结构体时声明记录和列表之间的嵌入关系。大多数情况下这并不重要,但某些情况下则刚好相反:
当然,如果你不想控制结构体定义,也不想管代码究竟被分配到哪儿,你还可以选择第三方的库来继续使用std::list。
写多线程代码时,你需要特别注意:一个删除操作会调用到所有侵入式的链接域的析构函数,来保证将该元素从列表中删除。
如果被删除的对象已经从所有列表中解链了,那当然没问题;但是如果对象仍然链接在某个列表上,这时就需要锁来防止竞争状态出现。下面是几个优化方案:
你可以看到我做了这么多工作,对吧?
给intrusive list分配对象链接或者复制其结构都是不可能的,同样也无法对列表进行上述操作。这也不是你会在实践中碰到的限制。对于需要将项目从一个列表移动到另一个的特殊情况,有必要专门写一个函数将两个列表中的元素拼接在一起。
Boost intrusive list.hpp实现了一个类似于我上面所写的intrusive list,它可以解决所有你遇到过的链表问题,因此它也非常难以使用。它太过复杂了——我认为完全不必如此。
如果你看过了源代码,我希望你能立刻察觉这点。首先,整个instrusive linked-list(侵入式链表),包括注释和MIT许可证,只有不到500行。
boost instrusive list.hpp和它相比显然功能更多,即使除去11!个充斥着难以理解的的现代(modern)C++模板附属的头文件也有1500行。
下面是我在ArenaSrv和StsSrv中实现的代码。我写的这个服务器框架几乎在所有Guild Wars系列(包括GW1和GW2)中都使用到了,但为了让代码更清晰、简介,又重写了一遍。
下面的代码是为了防止一个叫做Slowloris的特定网络服务攻击。Slowloris会逐步地把众多的套接字(socket)和一个网络服务联接起来,直到与服务器间的联接达到饱和状态,此时服务器将无法正常工作。虽然其它服务器也会有同样的问题,但Apache服务器尤其容易受到Slowloris攻击。
在这种情况下,你不会想用std::list的,因为每次从“close-me-soon”中删除一个联接时,平均需要遍历列表的50%。在Guild Wars 1的某些服务器上,有的进程会同时创建超过20000条联接,所以需要遍历10000条列表项目——这可不是一个好主意!
这种链表技术不是我发明的,我第一次碰到它是在Mike O’Brien为Diablo写的代码,也就是在上篇文章中提到的Storm.dll中。当我、Mike和Jeff Strain开始开发ArenaNet时,Mike首先开始的就是这个的链表优化版本。
快离开ArenaNet的时候,我发现在使用了intrusive list10年之后——一直没有给予这样愚蠢的链表bug足够重视——我发现需要重新实现它,因为现在还没有更好的替代方案(尽管有boost),但还是遇到了频繁的尝试和错误。
为了避免这些重复工作,我按照MIT协议开源了这些代码,所以你可以在非商业限制下使用它。
所以本文是对于intrusive list的使用说明,它会在使用完后自动清理(注意:多线程代码),用它编写出来的代码非常可靠。
Guild Wars的编程团队中包括十余个刚毕业的大学生,如果我们放任他们使用std::list编写游戏引擎,恐怕会带来难以计数的bug——我并没有冒犯他们的意思,他们确实很不错。通过让他们使用这样的工具,我们写出了超过6500000行代码——一个大型游戏——并且异常稳定、可靠。
稳定性是一个非常关键的编程指标。通过创建不错的集合类(collection classes),我们终将能够达到更远大的目标。
在第三篇中,我将着重讲解关键问题:它是怎么工作的?
等不及的话,可以好好看看这里List.h的实现。
原文链接:Code of Honer