最近的工作有:
1、研究delegate对于性能的影响;
2、研究对象处理方式;
3、思考对象计算问题。
下面分别展开叙述:
一、delegate的问题。
这个问题是因为考虑到游戏对象里面有很多需要互动的地方,那么到底如何进行互动就是一件值得探讨的问题了。首先,游戏互动总会有被动调用对象的情况发生,比如说对方打击了“你”一下,那么“你”就必须有相应的动作表现。那么我们可以用“事件”的方式、“接口”的方式或者用“直接调用”的方式解决。我这里不作详细的报告了,简单说一下试验的结果和最后的结论,详细的报告可以看我的另外一篇Post,或者这一篇Post有后继的一些想法。简单的说就是:一、delegate、interface以及directcall三种方式在单触发的情况下,性能相差比较小。二、在多触发的情况下delegate和其他方法的性能像差比较大,相差好几倍。三、在触发的代码里面包含了一个一千次的简单累加循环的情况下进行多触发,性能相差不大,大约为5%。测试的结果对于delegate不是非常有利,但是最后我们考虑到如下几点,还是不得不采用了delegate(event):
1、考虑情况三,性能相差并不是非常明显。在这里我们进行的是千万次级别的运算,性能相差大约0.5s。但这个仅仅是单纯测试这个方面的问题,如果摆到真正的成品里面,还会夹杂着大量其他运算,所以这个问题带来的性能影响会更低,基本上不应该是瓶颈所在。
2、考虑到多触发的情况下,无论使用interface还是directcall,都会给设计开发人员带来一定程度的麻烦。甚至对于引擎的设计来说还会需要一些额外的代码,而某些东西的实际上面可能还会有其他的不方便。光是为了实现多触发,我们就可能需要做不少的额外工作了。
3、考虑情况一,这个情况(单触发)应该是大部分的实际情形,因此我们很可能并不能够从使用interface/directcall上面获得预期的好处。那些预期的性能提升都是在多触发的情况下作测试得出的。
4、考虑到日后可能移植到Whidbey底下,可能这一性能差异根本就不会存在了。(这方面似乎有消息能够暗示delegate的性能问题会有比较令人感到满意的解决。)
二、对象处理方式
这个问题实际上说的是大量对象应该如何维护的问题。这个问题在很多关于设计游戏的文章里面都会提到,大部分都是建议自己维护一个很大的对象池,以此避免大量对象的构造工作。我开始也对这个说法抱有积极的看法,但是后来思考一下就会发现,这个工作在.NET里面是由GC来完成的。(比如说.NET的对象并不需要析构,比如说.NET的堆都是进行了相应的维护的。) 如果我们自己再维护一个对象池,那么很可能会有问题存在:1、我们的对象池性能可以和GC相比吗?正确性和安全性呢?2、我们的对象池会干扰GC的正常工作?比如某些对象我们不放到自己的对象池里面,由于这个对象池的存在占用了大量的空间,反而可能产生这个不再对象池的对象效率异常的低下。甚至有可能由于这个对象池本身过于庞大,本身就存在效率问题(GC无法把其中的一部分放在速度较快的GEN0里面,而只好全部放到比较大的GEN2里面了,等等)。3、自己设计一个对象池,那么整个的设计是否会比较麻烦呢?会不会造成最终游戏开发者的设计上的麻烦呢?
对于这个问题,我交给了mikespook来进行测试。我对于测试的要求是这样的:首先用大量的对象占用大小为N的内存,这个步骤模拟一些不会频繁释放的对象,作为基本的内存压力。其次,用大小为x,数量为y的小对象,以频率h在内存当中不断的产生和销毁,并且保证内存当中时刻有z个这样的对象存在。在模拟当中,N、x、y、h和z都应该是可以调整的,以期模拟出不同情况下的结果,看一下在什么样的情况下交给GC比较快,在什么样的情况下自己做对象池比较合理,或者在什么样的情况下实际上没有太大的差别。
最近mikespook自己在忙,可能没有时间仔细做,甚至连报告也没有时间给出来。昨天晚上他找到dudu,把结果简单的说了一下,他大概简单做了一下测试,结果表明至少GC比我们想象的要好。(我猜测这已经是相当保守的说法了,没有和mike直接聊,不清楚具体的情况。我之所以这么猜测的原因在下面这句话。)mike可能觉得这是因为对象池设计不良造成的,他找了一点资料,试图再次做一个实验,不过近期没有时间,可能要等一段时间才有结果。事实上我已经预料到GC的效果比我们自己来做要好,但是没有做过实验不知道,不放心,现在我觉得我可以这么说了:如果我们在.NET的架构下,能够写出一个比GC管理更有效的对象池结构,那会令我非常吃惊的,估计微软也会有点惊奇。原因是这样的:MS对于GC的考虑可以说是非常的周详,比Java的还要夸张。首先GC分为三代(Java是两代),gen0的大小和CPU的lv1 cache一致,gen1和lv2的一致,gen2才是“无穷大”的,这里的一致至少是数量级上面的。为什么这么分呢?因为一个研究表明,越老的对象就越不可能被释放,同时也访问的频率也越低。反过来说就是最新生成的对象基本上都会在短期内被访问到,而且通常是高频率访问。那么如果访问频率高的对象总是放在同一块内存当中,被CPU缓冲命中的可能性就越高!因为CPU总是试图缓冲一段连续的,经常被访问的内存块。如果我们自己写对象缓冲池,那么就很难考虑得那么仔细,那么清楚。同时还很可能影响GC的发挥,因为一个对象长期存在(对象池存在的根本原因就是充分利用一个已经构造出来的对象,让他长期存在),必然会被GC假设为访问频率低的对象,而很可能被转移到Gen1甚至Gen2里面去,效率就很可能会降低(经常缓冲不命中,需要从内存读取)。此外,还可能占用部分gen0的空间,使得一些不放在缓冲池进行处理的对象被迫转移到Gen1或者Gen2里面,不得不降低效率。而缓冲池还会占用大量的堆空间,使得GC的内存分配变得困难。如果大家看过一些关于.NET下面开发高效率程序的建议的话,那么应该看过这么几条:1、尽量少使用占用大量空间的对象结构;2、尽量少的进行大量对象的关联(就是我引用你,你引用他,他引用……这样的一长串);3、……
当然,如果mike新的测试表明缓冲池比GC效率高的话,我还是不得不重新进行慎重和仔细的考虑的。但是目前而言,我准备打算放弃“对象缓冲池”的想法了,准备进行下一步的设计和其他方面的试验。
三、思考对象计算问题。
这个问题逻辑上比较简单,但是要说明白还不是一件简单的事情。假设你控制Tank,要“攻击”一个我的Building,攻击的结果根据你的Tank的攻击力和我的Building的防御力计算出伤害值,以此修正我的Building的HP值。好,问题马上就出来了:你控制的Tank这个对象在哪里呢?我的Building在哪里呢?这个摧毁的动作怎么进行计算——如何得到双方的数据?计算在哪里进行?修正如何进行?
详细一点说,你要思考一下这样的代码怎么具体的写。首先说明一点,只要是网络游戏,无论你是C/S模式还是分布式,无论是小规模对战如C&C还是大规模游戏如传奇,都会存在的。比如说,我假设是C/S模式,又假设你采取的是server计算,然后把结果交给client,那么这个计算的结果怎么样才能够体现到client上面呢?是在网络上面传输给我(Building的控制者)“HP:-12”还是“Tank.ID=123; Action = Fire”?前一种方式下面,你打算怎么实现一个框架?你总不能让最终的游戏设计着这么设计一个游戏对象吧:
Class MetalTank
{
void OnReceivedMessage(string msg)
{
if (msg....) // parse the msg
}
}
这么做肯定没有人喜欢,如果你说摆到引擎里面的话,那么必然需要使用reflection,因为引擎根本就不知道你有哪些对象,有哪些属性。这样的话性能不好不说,不美观更是一个问题。当然,这么设计最终肯定能够运行起来,而能够运行起来不应该是我们的目标。
好,如果说是后面那种方式,那么Tank.ID=123的攻击力我如何获得?恩,难道我的机子上有Tank.ID=123的对象?假设有,那么姑且不论这个Tank.ID=123的对象我怎么获得,下面这几个问题我不得不提出来:
1、这个Tank.ID=123对象里面哪一些属性资料你需要知道?全部?还是部分?如何保持这些数据和对方机子上面的同步?如果全部数据你都有,那么是不是就是说你的机子上面的对象和对方的是一样的?双方都在做相同的计算?那么怎么可能分布式计算呢?(每个客户端都需要知道所有的信息,都需要计算所有的东西)
2、如果不应该知道所有的东西,而应该只是知道有这么一个对象以及一些基本的信息,那么是否意味着Tank这个类需要有两个版本?(实际上前面那个server计算只把结果发送给client的模式里面也有这个问题,server是一个完整的版本,而client是一个简单的版本。)这样的设计会不会很复杂?会不会增加了游戏设计者的大量工作?哪一些信息需要知道呢?哪一些不需要知道呢?万一设计失误把某些需要知道的弄成了不需要知道的,某些不需要知道的却变成知道的,发现了这个Bug需要改,会不会造成整个游戏的结构性调整呢?
写这篇Post的时候我还以为把这个问题考虑清楚了,后来想想,发现还是没有仔细考虑清楚,等我考虑清楚了再来报告。不过可以先透露一下,我还是坚持把计算放在Action这个类里面,但是很可能会对这个类进行一个修改,把Action的步骤分开成多部分,甚至可能再添加一些辅助用的类。此外,我希望所有对象在每一个客户端里面都存在,但是并不都进行计算。(可是这样的话又有一些其他问题出来了:我们很多时候是在计算的时候触发引擎的一些更新,例如GUI上面的一些变化,比如位置、HP条等等,但是如果有一些对象只是接受结果,有一些对象会进行计算,那么怎么用比较统一的方式来触发引清呢?)
看来需要思考的东西还有很多很多!
1、研究delegate对于性能的影响;
2、研究对象处理方式;
3、思考对象计算问题。
下面分别展开叙述:
一、delegate的问题。
这个问题是因为考虑到游戏对象里面有很多需要互动的地方,那么到底如何进行互动就是一件值得探讨的问题了。首先,游戏互动总会有被动调用对象的情况发生,比如说对方打击了“你”一下,那么“你”就必须有相应的动作表现。那么我们可以用“事件”的方式、“接口”的方式或者用“直接调用”的方式解决。我这里不作详细的报告了,简单说一下试验的结果和最后的结论,详细的报告可以看我的另外一篇Post,或者这一篇Post有后继的一些想法。简单的说就是:一、delegate、interface以及directcall三种方式在单触发的情况下,性能相差比较小。二、在多触发的情况下delegate和其他方法的性能像差比较大,相差好几倍。三、在触发的代码里面包含了一个一千次的简单累加循环的情况下进行多触发,性能相差不大,大约为5%。测试的结果对于delegate不是非常有利,但是最后我们考虑到如下几点,还是不得不采用了delegate(event):
1、考虑情况三,性能相差并不是非常明显。在这里我们进行的是千万次级别的运算,性能相差大约0.5s。但这个仅仅是单纯测试这个方面的问题,如果摆到真正的成品里面,还会夹杂着大量其他运算,所以这个问题带来的性能影响会更低,基本上不应该是瓶颈所在。
2、考虑到多触发的情况下,无论使用interface还是directcall,都会给设计开发人员带来一定程度的麻烦。甚至对于引擎的设计来说还会需要一些额外的代码,而某些东西的实际上面可能还会有其他的不方便。光是为了实现多触发,我们就可能需要做不少的额外工作了。
3、考虑情况一,这个情况(单触发)应该是大部分的实际情形,因此我们很可能并不能够从使用interface/directcall上面获得预期的好处。那些预期的性能提升都是在多触发的情况下作测试得出的。
4、考虑到日后可能移植到Whidbey底下,可能这一性能差异根本就不会存在了。(这方面似乎有消息能够暗示delegate的性能问题会有比较令人感到满意的解决。)
二、对象处理方式
这个问题实际上说的是大量对象应该如何维护的问题。这个问题在很多关于设计游戏的文章里面都会提到,大部分都是建议自己维护一个很大的对象池,以此避免大量对象的构造工作。我开始也对这个说法抱有积极的看法,但是后来思考一下就会发现,这个工作在.NET里面是由GC来完成的。(比如说.NET的对象并不需要析构,比如说.NET的堆都是进行了相应的维护的。) 如果我们自己再维护一个对象池,那么很可能会有问题存在:1、我们的对象池性能可以和GC相比吗?正确性和安全性呢?2、我们的对象池会干扰GC的正常工作?比如某些对象我们不放到自己的对象池里面,由于这个对象池的存在占用了大量的空间,反而可能产生这个不再对象池的对象效率异常的低下。甚至有可能由于这个对象池本身过于庞大,本身就存在效率问题(GC无法把其中的一部分放在速度较快的GEN0里面,而只好全部放到比较大的GEN2里面了,等等)。3、自己设计一个对象池,那么整个的设计是否会比较麻烦呢?会不会造成最终游戏开发者的设计上的麻烦呢?
对于这个问题,我交给了mikespook来进行测试。我对于测试的要求是这样的:首先用大量的对象占用大小为N的内存,这个步骤模拟一些不会频繁释放的对象,作为基本的内存压力。其次,用大小为x,数量为y的小对象,以频率h在内存当中不断的产生和销毁,并且保证内存当中时刻有z个这样的对象存在。在模拟当中,N、x、y、h和z都应该是可以调整的,以期模拟出不同情况下的结果,看一下在什么样的情况下交给GC比较快,在什么样的情况下自己做对象池比较合理,或者在什么样的情况下实际上没有太大的差别。
最近mikespook自己在忙,可能没有时间仔细做,甚至连报告也没有时间给出来。昨天晚上他找到dudu,把结果简单的说了一下,他大概简单做了一下测试,结果表明至少GC比我们想象的要好。(我猜测这已经是相当保守的说法了,没有和mike直接聊,不清楚具体的情况。我之所以这么猜测的原因在下面这句话。)mike可能觉得这是因为对象池设计不良造成的,他找了一点资料,试图再次做一个实验,不过近期没有时间,可能要等一段时间才有结果。事实上我已经预料到GC的效果比我们自己来做要好,但是没有做过实验不知道,不放心,现在我觉得我可以这么说了:如果我们在.NET的架构下,能够写出一个比GC管理更有效的对象池结构,那会令我非常吃惊的,估计微软也会有点惊奇。原因是这样的:MS对于GC的考虑可以说是非常的周详,比Java的还要夸张。首先GC分为三代(Java是两代),gen0的大小和CPU的lv1 cache一致,gen1和lv2的一致,gen2才是“无穷大”的,这里的一致至少是数量级上面的。为什么这么分呢?因为一个研究表明,越老的对象就越不可能被释放,同时也访问的频率也越低。反过来说就是最新生成的对象基本上都会在短期内被访问到,而且通常是高频率访问。那么如果访问频率高的对象总是放在同一块内存当中,被CPU缓冲命中的可能性就越高!因为CPU总是试图缓冲一段连续的,经常被访问的内存块。如果我们自己写对象缓冲池,那么就很难考虑得那么仔细,那么清楚。同时还很可能影响GC的发挥,因为一个对象长期存在(对象池存在的根本原因就是充分利用一个已经构造出来的对象,让他长期存在),必然会被GC假设为访问频率低的对象,而很可能被转移到Gen1甚至Gen2里面去,效率就很可能会降低(经常缓冲不命中,需要从内存读取)。此外,还可能占用部分gen0的空间,使得一些不放在缓冲池进行处理的对象被迫转移到Gen1或者Gen2里面,不得不降低效率。而缓冲池还会占用大量的堆空间,使得GC的内存分配变得困难。如果大家看过一些关于.NET下面开发高效率程序的建议的话,那么应该看过这么几条:1、尽量少使用占用大量空间的对象结构;2、尽量少的进行大量对象的关联(就是我引用你,你引用他,他引用……这样的一长串);3、……
当然,如果mike新的测试表明缓冲池比GC效率高的话,我还是不得不重新进行慎重和仔细的考虑的。但是目前而言,我准备打算放弃“对象缓冲池”的想法了,准备进行下一步的设计和其他方面的试验。
三、思考对象计算问题。
这个问题逻辑上比较简单,但是要说明白还不是一件简单的事情。假设你控制Tank,要“攻击”一个我的Building,攻击的结果根据你的Tank的攻击力和我的Building的防御力计算出伤害值,以此修正我的Building的HP值。好,问题马上就出来了:你控制的Tank这个对象在哪里呢?我的Building在哪里呢?这个摧毁的动作怎么进行计算——如何得到双方的数据?计算在哪里进行?修正如何进行?
详细一点说,你要思考一下这样的代码怎么具体的写。首先说明一点,只要是网络游戏,无论你是C/S模式还是分布式,无论是小规模对战如C&C还是大规模游戏如传奇,都会存在的。比如说,我假设是C/S模式,又假设你采取的是server计算,然后把结果交给client,那么这个计算的结果怎么样才能够体现到client上面呢?是在网络上面传输给我(Building的控制者)“HP:-12”还是“Tank.ID=123; Action = Fire”?前一种方式下面,你打算怎么实现一个框架?你总不能让最终的游戏设计着这么设计一个游戏对象吧:
Class MetalTank
{
void OnReceivedMessage(string msg)
{
if (msg....) // parse the msg
}
}
这么做肯定没有人喜欢,如果你说摆到引擎里面的话,那么必然需要使用reflection,因为引擎根本就不知道你有哪些对象,有哪些属性。这样的话性能不好不说,不美观更是一个问题。当然,这么设计最终肯定能够运行起来,而能够运行起来不应该是我们的目标。
好,如果说是后面那种方式,那么Tank.ID=123的攻击力我如何获得?恩,难道我的机子上有Tank.ID=123的对象?假设有,那么姑且不论这个Tank.ID=123的对象我怎么获得,下面这几个问题我不得不提出来:
1、这个Tank.ID=123对象里面哪一些属性资料你需要知道?全部?还是部分?如何保持这些数据和对方机子上面的同步?如果全部数据你都有,那么是不是就是说你的机子上面的对象和对方的是一样的?双方都在做相同的计算?那么怎么可能分布式计算呢?(每个客户端都需要知道所有的信息,都需要计算所有的东西)
2、如果不应该知道所有的东西,而应该只是知道有这么一个对象以及一些基本的信息,那么是否意味着Tank这个类需要有两个版本?(实际上前面那个server计算只把结果发送给client的模式里面也有这个问题,server是一个完整的版本,而client是一个简单的版本。)这样的设计会不会很复杂?会不会增加了游戏设计者的大量工作?哪一些信息需要知道呢?哪一些不需要知道呢?万一设计失误把某些需要知道的弄成了不需要知道的,某些不需要知道的却变成知道的,发现了这个Bug需要改,会不会造成整个游戏的结构性调整呢?
写这篇Post的时候我还以为把这个问题考虑清楚了,后来想想,发现还是没有仔细考虑清楚,等我考虑清楚了再来报告。不过可以先透露一下,我还是坚持把计算放在Action这个类里面,但是很可能会对这个类进行一个修改,把Action的步骤分开成多部分,甚至可能再添加一些辅助用的类。此外,我希望所有对象在每一个客户端里面都存在,但是并不都进行计算。(可是这样的话又有一些其他问题出来了:我们很多时候是在计算的时候触发引擎的一些更新,例如GUI上面的一些变化,比如位置、HP条等等,但是如果有一些对象只是接受结果,有一些对象会进行计算,那么怎么用比较统一的方式来触发引清呢?)
看来需要思考的东西还有很多很多!