Fatbean 2014-08-17T02:00:29+00:00 silver.accc@gmail.com Dig Into Time_wait 2014-08-17T00:00:00+00:00 Fatbean http://cortexiphan.github.com/2014/08/Dig-Into-TIME_WAIT TIME_WAIT状态小探

这篇文章是对网上一些文章整理之后的总结,主要讨论TIME_WAIT状态的问题以及解决方法。

TIME_WAIT状态以及它的作用

TCPstate (引自vincent.bernat.im)

从TCP状态转移图中可以看到,一个TCP连接中,只有主动关闭,即先发送FIN包的那一端会最终进入TIME_WAIT状态。当然,如果两端同时关闭连接,即在收到对端FIN之前就发出FIN包,那么两端最终都会进入TIME_WAIT状态。

TIME_WAIT状态有两个作用:

防止上一个连接的延时包被新连接当做正常包接收

一个TCP连接由一个4元组(source_ip、source_port、destination_ip、destination_port)唯一标识。一个旧连接关闭后,如果一个具有相同4元组的新连接马上建立起来,这时旧连接的一个包因为延时刚刚到达,并且它的序列号刚好在新连接的接收窗口中,那么新连接就会把这个包当做正常包接收,进而使数据出现混乱。虽然每次建立连接时使用的序列号都是随机产生的,但是序列号的长度只有32位,在高速网络中可能会很快出现序列号循环,这种情况下序列号刚好在接收窗口中的可能性就高很多。TIME_WAIT的2MSL过去之后,我们可以确信旧连接的延时包都已经在网络上消失,不可能再干扰到新连接了。 delayed_segment (引自www.serverframework.com)

确保对端可靠地关闭当前连接

这里又可以从两个方面考虑

  • 为了可以重传最后的ACK。在对端发出的FIN包丢失或者本端发出的最后ACK丢失时,本端需要重传最后的ACK。对端发出FIN包之后一旦超时没有收到ACK,就会重传FIN包,本端需要能够响应重传的FIN包,所以需要留在TIME_WAIT状态一段时间。至于TIME_WAIT的时间为什么是2MSL,而不是MSL,甚至RTO的某个函数,我想原因应该是这样的:
  • 确保在本端试图建立相同4元组的新连接时,对端的旧连接已经关闭。TIME_WAIT状态结束之后,本地就可以建立一个新的、具有相同4元组的连接,如果这时对端仍处在LAST_ACK状态,那么本地发过去的SYN包就会被对端抛弃,并且用RST响应,导致建立连接失败。 last_ack_long (引自vincent.bernat.im)

TIME_WAIT对系统性能的影响

TIME_WAIT对系统的影响主要有以下3个方面

导致系统无法新建一个与TIME_WAIT套接字相同4元组的连接

当然,这也是设置TIME_WAIT状态的目的。对于发起主动连接的客户端来说,不能建立这样的连接也不是什么大问题,因为默认情况下系统会为套接字选择一个可用端口,除非我们再connect之前执行了bind。需要注意的是,如果客户端需要频繁发起并主动关闭连接,这可能就会出现问题,太多的端口处于TIME_WAIT状态,导致系统可选择的端口耗尽,那也会无法建立新连接。

导致程序无法立即重启

很多系统的TCP实现了比RFC要求的更严格的标准,RFC只要求不能存在两个具有相同4元组的连接,而很多系统则要求本地ip和端口被某个socket占用之后,不能够再执行bind。所以,如果有TIME_WAIT的套接字占用了本地程序监听的ip和端口,那本地程序重启是将会bind失败。

CPU和内存消耗

保存TCP连接结构需要消耗内存消耗,查询可用端口时也有CPU消耗。但这些消耗都很小,与数据处理的消耗相比,可以忽略。

解决TIME_WAIT问题的方法

什么时候才需要TIME_WAIT的问题?

  • 主动发起连接的一方容易受到TIME_WAIT的影响。如果TIME_WAIT状态的socket大量积累,那么系统能够选择的可用端口就会减少甚至消耗殆尽,这是本地就不能发起连接了。
  • 被动接受连接的一方较少受到TIME_WAIT的影响。被动接受连接的服务器一般使用众所周知的地址和端口,即使有很多socket处于TIME_WAIT状态,这个地址和端口也只构成了TCP连接4元祖的一部分,只要客户端的的ip不同,或者使用的端口不同,新建连接就不会有问题。除非某个客户端的本地端口耗尽才会造成无法建立连接,但这也只是个别客户的问题,对其他客户没有影响。

通过添加资源来减轻TIME_WAIT的影响

  • 前面提到客户端可能存在本地端口耗尽的情况,我们可以扩大系统可以选择的本地端口范围来减轻这个问题,但是能够增加的两不多,只是聊胜于无。
  • TIME_WAIT只是限制具有相同4元祖的的连接,我们只要增加4元祖中的任何一个,就可以减轻TIME_WAIT的问题,比如增加服务器的ip(让同一个域名对应多个ip),增加监听的端口(http建通80、81、82...),客户端使用多个ip,增加系统可选择的端口范围等。

缩短TIME_WAIT的时间

按照一般的想法,既然TIME_WAIT的让我们长时间不能新建连接,那么可以考虑把TIME_WAIT的时间缩短,即修改MSL的时间。不过这很可能解决不了问题,当问题已经严重到port range里面已经没有可用端口时,TIME_WAIT的时间变短,很可能只会适得更多的短连接建立并且关闭,端口仍然会被TIME_WAIT沾满。

避免TIME_WAIT的出现

  • 按照陈浩的说法,既然TIME_WAIT是主动关闭连接那一边才出现的状态,那就让对端先关闭连接得了,这样TIME_WAIT的破问题就是对方的了。需要关闭连接时,服务器可以在应用层通知客户端主动关闭连接,这样TIME_WAIT状态就留给了客户端,而客户端一般不会受到TIME_WAIT的困扰,只要它不需要主动建立大量的短连接。
  • 更激进一点的,服务器可以不进行优雅的四次挥手断开连接,而是直接发送RST包,这样当然就不会有TIME_WAIT状态了。但是,这种方法会导致缓冲区里的数据被丢弃,所以程序需要在应用层保证这种结果是可接受的,比如上面服务器通知客户端主动断开连接,但客户端超时不发送FIN包,那服务器就可以放松RST包以放弃连接。
  • 设置socket的option:SO_LINGER。设置socket为不等待之后,socket在断开连接时直接跳过TIME_WAIT阶段,直接发送RST包。

设置SO_REUSEADDR

前面提到过,某些系统上一个ip和端口已经被一个socket占用时(比如TIME_WAIT或者FIN_WAIT_2状态),bind会失败,导致程序退出后马上重启的失败。设置socket的option为SO_REUSEADDR后,这样的bind就可以成功。 一些文章提到SO_REUSEADDR只是让程序可以在一个已经被占用的端口上bind的成功,而具有相同4元组的连接仍然不允许建立。但笔者在Debian上实验后发现,服务端设置了SO_REUSEADDR之后,并且正在listen的端口有socket处于TIME_WAIT状态,客户端用TIME_WAIT对应的端口连接服务器,服务器会直接重用TIME_WAIT的socket建立连接。

设置tw_reuse

tw_reuse默认是关闭的,打开之后,可以使新连接直接重用旧的TIME_WAIT状态的连接来建立(新旧连接具有相同的4元组),只要新连接的时间戳严格大于旧连接。这就要求两边的TCP都支持timestamp的选项。当然,TCP协议还是要保证TIME_WAIT状态所提供的两个功能得以实现:1)旧连接的延时包不能干扰新连接,这个通过时间戳来保证。2)本地就连接结束,新连接建立时,对方的旧连接已经关闭,这里假设本地建立新连接时,对方仍处于LAST_ACK状态,对方收到SYN包后,仍然返回已经发送过的FIN和ACK,而本地不认识这些包,直接返回RST,导致对方LAST_ACK的结束,这之后本地再发送SYN就可以成功建立连接了。

设置tw_recycle

tw_recycle比tw_reuse还要激进,tw_reuse只是在必要时(新旧连接具有相同的4元组)才重用TIME_WAIT状态的连接,而tw_recycle直接提前结束TIME_WAIT状态的连接。这是TCP协议会仍旧记录所有TIME_WAIT状态连接的时间戳,对于该4元组上的连接的数据包,如果其时间戳不能严格大于该记录的时间,则直接被丢弃。NAT背后的机器很可能时间不能同步,这时下会出现连接不上的情况。

总结

虽然有时候TIME_WAIT会给我们带来一些麻烦,但这仍然是TCP协议中的一个非常必要的好东西。如果想享受TCP带来的可靠保证,又不想受到TIME_WAIT的影响,那最好的办法是让对方关闭连接;如果应用层可以确保不经过四次握手关闭也能保证程序逻辑正确,那直接通过RST包中断连接也是一个方法;实在不行,再考虑设置tw_reuse或者tw_recycle,按照RFC的建议,这两个参数只有在经过非常专业的分析之后才应该设置。

引用

[1] TIME_WAIT and its design implications for protocols and scalable client server systems

[2] Coping with the TCP TIME-WAIT state on busy Linux servers

[3] Bind before connect

]]>
Python Multiprocessing Queue 2014-07-18T00:00:00+00:00 Fatbean http://cortexiphan.github.com/2014/07/Python-Multiprocessing-Queue One of my old post from CSDN.

import multiprocessing, Queue
from multiprocessing import Process
from time import sleep
from datetime import datetime

class MultiProcessProducer(multiprocessing.Process):
    """"""

    #----------------------------------------------------------------------
    def __init__(self,num, queue):
        """Constructor"""
        multiprocessing.Process.__init__(self)
        self.num = num
        self.queue = queue

    def run(self):
        t1 = datetime.now()
        print 'producer start', self.num, t1
        for i in range(1000000):
            self.queue.put((i, self.num))
            #print 'producer put', i, self.num
        t2 = datetime.now()
        print 'producer exit', self.num, t2
        print 'producer', self.num, t2-t1


class MultiProcessConsumer(multiprocessing.Process):
    """"""

    #----------------------------------------------------------------------
    def __init__(self,num, queue):
        """Constructor"""
        multiprocessing.Process.__init__(self)
        self.num = num
        self.queue = queue

    def run(self):
        t1 = datetime.now()
        print 'consumer start', self.num, t1
        while True:
            d = self.queue.get()
            if d != None:
                #print 'consumer get', d, self.num
                continue
            else:
                break
        t2 = datetime.now()
        print 'consumer exit', self.num, t2
        print 'consumer', self.num, t2-t1

def main():
    #create queue
    queue = multiprocessing.Queue()

    #create processes
    producer = []
    for i in range(1):
        producer.append(MultiProcessProducer(i, queue))

    consumer = []
    for i in range(2):
        consumer.append(MultiProcessConsumer(i, queue))

    #start processes
    for i in range(len(producer)):
        producer[i].start()

    for i in range(len(consumer)):
        consumer[i].start()

    #wait for processs to exit
    for i in range(len(producer)):
        producer[i].join()

    for i in range(len(consumer)):
        queue.put(None)

    for i in range(len(consumer)):
        consumer[i].join()

    print 'finish'


if __name__ == "__main__":
    main()
]]>
Data Locality 2014-07-17T00:00:00+00:00 Fatbean http://cortexiphan.github.com/2014/07/Data-Locality 内存数据局部性(Data Locality)

这其实是《Game Programming Patterns》中的一篇文章,它主要讲述了数据在内存中的布局是如何影响CPU缓存(CPU Caching),进而让程序性能产生巨大变化的。作者还没有写完,我就一直在关注这本书,这里面的大部分文章都值得一看,比如《Game Loop》,但这次先只翻译一篇。

Herb Sutter的演讲和Scott Meyer的演讲都提到了CPU缓存对性能提升的重要性,并且Herb还专门提到了《Data Locality》这篇文章,还用了这篇文章的数据作为论据,这促使我决定马上把它整理成中文了。

目标

将数据布局设置得和CPU更亲和,以提高内存访问速度。

动因

摩尔定律给我们的印象是,CPU的速度一直在增长,似乎程序员连指头都不用动一下,程序的效率也会自然而然地提高。呃,芯片的频率确实一直在提高(其实CPU的频率也基本限制在10GHz的数量级,这值得我另写一篇文章了),但这只是代表我们处理数据的速度越来越快了,但我们获取数据的速度就没那么快了。

虽然你的牛X的CPU能够在短时间内做大量的数据运算,但也得先把数据从内存中读出来,倒到寄存器里面去才行。然而,上面的图就说明了内存的读写速度被CPU甩好几条大街了。当今的硬件环境下,从内存中读取一个字节的数据需要花好几百个时钟周期,假设大多数指令都需要花几百个时钟周期来获取数据,那我们的CPU如何不会浪费99%的时间在等待数据上?实际上,CPU确实有大量的时间是堵在等待内存数据上,但是情况也不是那么坏,下面我们开始用一个比喻来说明这个情况。

数据仓库

假设你是一个小办公室里的会计师,你的工作就是拿到一盒子文件,然后根据这一盒子文件做一些加加减减的会计工作。当然,你是根据一些只有会计师才懂的神奇逻辑来指定要拿那个盒子的文件的。由于你的勤奋努力、天资聪颖或者其他的什么原因,你能够在一分钟之内处理完一盒子文件。但是,还有一个小问题,所有那些装文件的盒子都存放在另一栋楼里的一个仓库中。要拿到文件盒子,你只能请仓库管理员送过来给你——他会开着一辆叉车在堆积如山的仓库中慢慢转,直到找到你要的那盒文件。这得花上他一整天的时间来拿到你要的文件盒。不像你,他没什么希望获得月度优秀员工的称号。这意味着无论你的速度有多快,你一天只能处理一个盒子的文件,剩下的时间里,你只能坐着思考人生。

直到有一天,一波搞工业设计的人出现了,他们的工作就是提高效率,比如让流水线走得更快之类的。他们在观察了你好几天之后,发现了一些特点:

  • 很多情况下,当你处理完一盒子文件后,你需要的下一个盒子也就在仓库中当前盒子的旁边。
  • 用一个叉车来运送仅仅一个盒子的文件实在有些浪费。
  • 你的办公室的角落其实还是有一点多余的空间的。

他们想出了一个很聪明的解决方案:每次你让仓库管理员给你去一盒文件时,他除了取回你指定的之外,还把旁边一整排文件盒子都给你送过来,他不知道你是否需要那些盒子的文件(鉴于他的工作积极性,他其实也不关心),他只是把尽可能多的文件盒子放到叉车上给你送过来,放到你办公室的角落里。

当你在找下一个文件盒子的时候,你首先要做的是看一看它在不在你办公室角落的那一对盒子中。如果在它在里面就好办了,你只需要花一秒钟的时间把那个盒子搬到桌子上,然后就可以继续你的加加减减了。如果办公室角落那里可以堆50个盒子,并且碰巧所有你需要的盒子都在这个盒子堆里,你的工作效率将提高50倍!

但是,如果你需要的文件盒不在那一排里面,那你就回到原点了。因为你的办公室职能放得下一排盒子而已,仓库管理员只能先把你办公室的盒子先运回去,然后再给你送一排新的过来。

给CPU用的一排盒子

凑巧的是,这与现代的CPU的工作方式十分相似。可以想象,你来扮演CPU的角色,你的办公桌就是CPU的寄存器,一盒子文件就是刚好能装满寄存器的数据,仓库就是你的内存,那个烦人的仓库管理员就是从内存往寄存器取数据的总线。

如果我(指作者,译者这时候还没出生)是30年前写这篇文章,比喻就到此为止了。但随着处理器越来越快,而内存却没有跟上,硬件工程师们就开始寻找解决方案了,比如说,CPU缓存。

现代计算机的芯片上有一小片缓存,CPU从这里读数据会比从内存中读数据要快得多。这片缓存之所以设置得很小,一方面是因为要集成到芯片上,另一方面,是因为这种类型的缓存(SRAM)比内存(RAM)贵多了(RAM里面的一个比特其实就是一个电容,而SRAM里面的一个比特是由4到6个三极管组成的电路保存的——译者注)。

这一小片存储空间就是缓存(Cache),而直接集成到芯片上那一小部分就是一级缓存(L1 Cache)。在上面的类比中,就是那一排文件盒子。每当你的芯片需要从内存读取哪怕一个字节的数据,一段连续的内存就会被读进来(一般是64~128字节)并放入缓存中。这用来放一排文件盒子的地方,就叫一个缓存行(Cache line)。

如果你需要的下一个字节数据刚好在这个缓存行中,那CPU就可以直接从缓存行中读出来,这比从内存中读取快多了。成功地从缓存中找到数据叫作Cache hit,缓存中找不到而需要去内存中找的时候就叫作Cache miss。

出现cache miss之后,CPU就只能停下来,无聊地等了好几百个时钟周期之后,数据终于从内存里面读进来,它才能执行下一条指令。当然我们的任务就是避免这种情况的发生啦。试想如果你要优化下面这样一段影响性能的关键代码:

for (int i = 0; i < NUM_THINGS; i++)
{
    sleepFor500Cycles();
    things[i].doStuff();
}

你准备做的第一件事情是什么?对,删掉那个没有意义有沉重的sleep函数调用。那个函数调用对性能的损耗和一次cache miss是一样的,所以你每次从内存读数据的时候,你就像是加了一行sleep这样的延时代码一样。

且慢,数据就是性能?

当我(作者)开始准备这一章的内容时,我写了一个用来触发最优条件和最差条件的小程序,我非常想亲眼看看缓存猛烈抖动的时候是怎样的一个惨烈。

结果出来之后,我吃了一斤。我预料到缓存抖动是个大事儿,但亲眼看见的感觉完全不一样。我写了两个几乎完全一样的程序,唯一的区别就是它们会引发不同程度的cache miss,而慢的那个比另一个慢了50倍。

这简直让我大开了眼界,我曾以为性能就是代码的事,而不是数据的事。一个字节并没有快慢之分吧,它只是一个静态的东西而已,但是有了缓存,数据的分布就会大大地影响性能了。

现在要做的就是怎么把上面的东西揉成一个章节了。缓存优化是一个很大的课题,我还没说到指令缓存,别忘了,内存中的代码也是要接在到CPU中才能执行的。这个课题值得一些专家写一整本书了。

既然你已经在看我这篇文章,我就告诉你几个基本原则,让你可以开始思考数据结构是如何影响你的程序性能的。说到底其实很简单:每当CPU要读取内存的时候,它会读取一整个cache line,你能把越多你可能用到的数据挤到这个cache line里面,你就跑得更快。于是,我的目标就是,组织好你的数据结构,使你要处理的内容在内存中是相邻的。

换句话说,如果你的代码要处理A,然后B,然后C,那你最好让它们在内存中的布局是这样的:

需要指出的是,这里的A、B、C课不是指向数据的指针,而就是它们自己,一个接一个地排在一起。一旦CPU要读A,它就会把B和C也读进去(当然这取决于A、B、C有多大,以及cache line有多大),当CPU开始处理B的时候,B已经在缓存里了,皆大欢喜啊!

模式

现代CPU使用缓存来加速内存访问,所以访问刚访问内容旁边的内容的速度会快很多。提高数据局部性——让数据按照你要处理它们的顺序排列以提高内存访问速度。

什么时候该用这个模式呢?

和大多数优化一样,首要的原则是,当你真的遇到了性能问题的时候,才考虑优化它。不经常执行到的代码就不要浪费时间优化了。没必要的时候进行优化只会让你的生活更苦逼,因为那样几乎一定会导致代码复杂化和不灵活。

具体到这个模式,你还必须确认你的性能问题的确是由cache miss引起的,如果你的代码是因为其他原因跑得慢,这个模式帮不了你。

简单的方式是手动加一下监控代码,看看两个代码点之间究竟花了多长时间,可能你需要一个精确一点的计时器。要捕获cache miss,你还需要一些更复杂的工具,因为你真的需要了解你的程序会在哪里、发生多少cache miss。

幸运的是,外边已经有很多工具可以做这个事情了,花点时间倒腾其中一个,并且确保你明白它输出的一大堆数据是什么意思,你再开始对你的代码开刀吧。

我只是说,cache miss会影响程序的性能,但你也不应该花大量时间做超前的缓存优化,当然,设计的时候尽可能地考虑缓存亲和性还是必要的。

注意

软件架构的一个主要特点就是抽象化,本书(指Game Programming Patterns)的一大部分也是在讲如何解构各部分代码,以便使它们更方便各自更新。在面向对象语言中,这基本上就意味着接口。

在C++中,使用接口就意味着通过指针或者引用访问对象。但是使用指针就意味着在内存中跳来跳去,导致本篇文章要避免的cache miss问题。

为了满足这个模式,你得牺牲一些代码的抽象性。你为你的程序做的数据局部性越多,你就得放弃更多的继承和接口,以及那些工具带给你的便利。天下没有十全十美的解决方案,就看你怎么取舍了,当然这也正是乐趣所在。

样例代码

如果你一定要走上数据局部性优化这条不归路,好吧,你会发现无数种方式来切分你的数据,以便你的CPU能更容易处理它们。作为开始,每一种常见的组织数据方式我都会用一个例子来说明,我的例子会以一个游戏引擎为背景,但注意背后的基本技术是可以应用到其他地方的。

连续数据

让我们从一个处理大量实体的游戏循环开始吧。这些实体根据领域被分解成了不同的部分——AI,物理模块,渲染模块,比如这样:

class GameEntity
{
public:
    GameEntity(AIComponent* ai,
        PhysicsComponent* physics,
        RenderComponent* render)
    : ai_(ai), physics_(physics), render_(render)
    {}

    AIComponent* ai(){return ai_;}
    PhysicsComponent* physics(){return physics_;}
    RenderComponent* render(){return render_;}
private:
    AIComponent* ai_;
    PhysicsComponent* physics_;
    RenderComponent* render_;
};

每种组件都只有一组相当少的状态,可能是几个向量或者矩阵,以及一个更新它的函数。它内部的细节并不重要,只要想象大概是这样的就好了:

class AIComponent
{
public:
    void update(){/* Work with and modify state... */}
private:
    //Goals, mood, etc. ...
};

class PhysicsComponent
{
public:
    void update(){/* Work with and modify state... */}
private:
    //Rigid body, velocity, mass, etc. ...
};

class RenderComponent
{
public:
    void render(){/* Work with and modify state...*/}
private:
    //Mesh, textures, shaders, etc. ...
};

游戏维护了一个巨大的指向虚拟世界中各个实体的指针数组。游戏循环的每一轮运转,我们都要顺序执行下面的步骤:

  • 更新所有实体的AI组件。
  • 更新所有实体的物理组件。
  • 通过它们的渲染组件渲染它们。

很多游戏引擎都是这样来做的:

while (!gameOver)
{
    // Process AI.
    for (int i = 0; i < numEntities; i++)
    {
        entities[i]->ai()->update();
    }

    // Update physics.
    for (int i = 0; i < numEntities; i++)
    {
        entities[i]->physics()->update();
    }

    // Draw to screen.
    for (int i = 0; i < numEntities; i++)
    {
        entities[i]->render()->render();
    }

    // Ohter game loop machinery for timing...
}

在你没有听说CPU缓存之前,上面的代码看起来似乎没什么问题。但现在,你回隐隐约约感觉到有点不对劲了。这代码不仅是在抖动缓存,它简直是在来来回回把缓存搞成浆糊了。看看它都做了什么:

  • 游戏实体的数组是一个指向实体的指针,所以对于数组的每一个元素,我们都得提领指针,这是一个cache miss。
  • 然后游戏实体还有一个指向组件的指针,这又是一个cache miss。
  • 然后我们调用组件的更新函数。
  • 现在我们回到步骤一,对每一个实体的每一个组件重复一次。

可怕的是,我们根本不知道这些对象在内存中是怎么分布的,而只能听凭内存管理器的发落了。因为实体不断地被分配,一段时间之后被销毁,堆内存很可能变成很随机的分布了。

如果我们的目的是在游戏地址空间里来一个类似“256M内存4日游”之类的廉价旅游套餐,这倒是个不错的选择。可惜我们的目的是让游戏跑得更快,而在内存里面到处闲逛显然不是一种好方法。还记得sleepFor500Cycles()函数吧?这段代码简直就是在不断地调用这个函数。

好吧,让我们把代码改得好一点吧。首先可以观察到,我们通过指针来获取游戏实体的唯一原因是,我们可以马上访问下一个实体的指针。游戏实体本身并没有什么有意思的状态,或者有用的函数,它内部的组件才是游戏循环关心的。

与其在地址空间这个漆黑夜空中撒上无数繁星点点的游戏实体和组件,我们还是回到地球上来吧。我们为每种组件维护一个巨大的数组:AI组件、物理组件和渲染组件各自一个普通的数组。比如这样:

AIComponent* aiComponents = new AIComponent[MAX_ENTITIES];
PhysicsComponent* physicsComponents = new PhysicsComponent[MAX_ENTITIES];
RenderComponent* renderComponents = new RenderComponent[MAX_ENTITIES];

我要再说一次,这些是组建的数组,不是指向组件的指针数组。数据一个字节接一个字节地就在那儿了。游戏循环可以直接这样访问了:

while (!gameOver)
{
    // Process AI.
    for (int i = 0; i < numEntities; i++)
    {
        aiComponents[i].update();
    }

    // Update physics.
    for (inti = 0; i < numEntities; i++)
    {
        physicsComponents[i].update();
    }

    // Draw to screen
    for (int i = 0; i < numEntities; i++)
    {
        renderComponents[i].render();
    }

    Other game loop machinery for timing...
}

我们丢弃了所有那些指针跳转,不再在内存里面跳来跳去,而是直接在3个连续数组爬过去就可以了。

这样我们把一个稳定字节流直接放到CPU饥饿的嘴巴里去了。在我的测试中,这个修改可以让整个循环比原来快50倍。

有趣的是,我们并没有丢失很多封装性。的确,游戏循环现在直接就更新各个组件,而不是通过游戏实体来更新了,但我们之前那样也只是为了保证能按照正确的顺序处理它们而已。即使是现在这样,每个组件本身还是得到了良好的封装性,它拥有自己的方法,我们只是修改了它调用方法的方式。

这个修改并不意味着我们也需要丢弃游戏实体这个类。我们可以就像以前那样保留指向其组件的指针,只是它们现在会指向那些数组而已。这样,当你想向游戏的其他模块传递游戏实体这个概念时,所有的内容都会跟过去。重要的是,影响性能的关键部分已经直接访问到数据了。

(未完待续,文中的图片链接也是Bob Nystrom个人网站上的)

]]>
Andromeda Galaxy And The Milky Way 2014-05-14T00:00:00+00:00 Fatbean http://cortexiphan.github.com/2014/05/Andromeda-Galaxy-And-The-Milky-Way

What do we know about The Andromeda Galaxy?

Well, this is The Andromeda Galaxy, the big brother of our galaxy family named Local Group. It is a spiral galaxy a little larger than our Milky Way Galaxy about 2.5 million light-years away. And it hosts about a trillion stars. You can get a whole bunch of other statistic data from the wikipedia. The Local Group is what I'm going to talk about. It consists of The Andrameda Galaxy, The Milky Way Galaxy and several dozens of smaller galaxies. These galaxies are bounded by gravitational pulls and orbiting the mass center of the Local Group. Well, the galaxies may not orbitting as you think as the planets orbitting the Sun. Because their orbits may not be circles or not even ellipses, but some weird trajectories. Sometimes they get closer, and sometimes they are faraway. Sometimes a smaller galaxy get ejected out to nowhere when several galaxies dance together. And sometimes two galaxies collide into each other when they get too close.

Does it have anything to do with us?

Sure it does, because The Andromeda Galaxy is moving towards us! The first time I realized that The Andromeda Galaxy was getting closer is the time when I watch the documentary series Wonders of the universe back in 2011. They said that The Andromeda Galaxy will 'meet' our galaxy in about 4 billion years from now, according to the sight-line speed infered from the doppler effect. While the side-way speed remains unknown so currently we are not sure whether it would be a near miss or a direct hit. Wow, 4 billions years? That's before the Sun could devour the Earth. In mid 2012, not long after i finished the series Wonders of the universe, a big news came out. Scientists have confirmed that the meeting of the two galaxies in 4 billion years is not a near miss, but a head-on collision. Scientists analyzed the data collected by Hobble Space Telescope of the pass several years and finally concluded that the side-way speed is relatively small compared to the sight-line speed. Sounds like the scientists have another problem to worry about. When the collision happens, the solar system are sure to be influented. There is a good chances that the Solar system getting farther out from center in the newly combined galaxy, or a smaller probability get ejected out even farther to the empty space of the universe.

Is it really a big deal?

When the two galaxies collide, billions of stars merge together to form a new elliptical galaxy. New stars will be borned, and shape of the galaxy will change dramatically. But, our Solar system as an individual will remain intact. The probability of two star colliding with each other is really small, because the distance between stars is way bigger than the size of themselves. So, the Sun will function as normal and the planets' orbits are not disturbed(if the Sun hasn't devoured them). The biggest change will be the view of the night sky. There won't be a Milky Way anymore, but another fantastic view. However, if we are ejected out from the galaxy, much fewer bright stars can we obeserve in the night sky.

]]>
All About Programming 2014-05-10T00:00:00+00:00 Fatbean http://cortexiphan.github.com/2014/05/All-About-Programming Welcom here! This is my blog. I will share my ideas about programming here.

]]>