最近在研究网络层性能的优化,IO的设计现在都有比较成熟的方案,比如使用Windows下的IOCP和linux下的多路复用(epoll等)模型,或者直接使用libuv,libev等封装的比较完善的Reactor模型。所以,主要考虑的还是数据方面的优化,这里也不谈论多线程锁的使用和可能进行的优化,剩下的也就是数据拷贝和内存回收了。
其实想到这里主要是因为我们游戏框架的数据包发送效率有很大的改善空间,比如对于一个已经构建完成的数据包,每次发送之前都需要拷贝出一个完全相同的数据包,然后再调用发送接口或者推送到一个发送队列。对于小数据包来说,内存拷贝对性能的影响可以忽略不计,但对于大数据包或者大量广播数据包,性能损失就不容忽视了。防止内存拷贝,保证数据指针在内存中只有一份,解决方法就是对数据包添加一个引用计数的原子变量,发送之前增加计数(原子操作),成功后减少计数,计数减至0时回收内存。
我们之前没有这么做是因为一个数据包发送之后,逻辑层可能还会继续对数据包进行写操作。对于将数据包作为userdata的lua对象来说,也无法保证发送之后lua层不会继续调用写包接口,而且我个人观点是不应该阻止逻辑层的这种行为。乍想问题似乎无解了,但其实已经解决了!还是从引用计数上着手,加上写时拷贝机制:数据包创建时,默认引用计数为1,调用发送接口之前,引用计数加1(原子操作)。对于任意数据包被推送到的线程或者原发送数据包的逻辑线程(即拥有数据包的线程),若判定其引用计数大于1则表示数据可能正在被多线程占用,这时触发写操作时(读操作和发送操作都可直接使用),再拷贝出一个完整的数据包(cow),之后的操作就完全隔离了,若拥有数据包的线程将引用计数减1,则表示该线程放弃数据包的使用权,之后对数据包的操作将无法预测。刚开始还想这样做会不会因为每次的写操作都会做引用计数的原子判断对效率产生影响,后来想到只要原子计数的类型小于cpu数据总线的宽度的话,原子判断即不必须了(把计数定义为volatile的,我对这一点儿也没有太大信心,希望大牛能指点)。
上面我们解决了数据包的线程安全问题,即对于能拿到数据包的线程来说,读写都是线程安全的了。对于内存回收重利用,在数据包引用计数减为0时,可将数据包放入回收队列中,个人考虑是每个线程一个回收队列,可以尽量避免锁竞争的问题,另外,对不同内存大小的数据包可根据大小进行分段处理,避免再次使用数据包时出现内存反复申请的问题(不然回收重利用机制的意义也就不大了)。