万胜 发表于 2021-2-19 09:36

Unity 网络消息打包的优化

引言

网络消息的打包是游戏中常见的需求,它所做的事情是将场景里各种对象的数据,或者是玩家数据打包并发送给服务端。
在 Unity 里常用的网络通信模块是 mono 的 System.Net.Sockets 下的套接字接口,发送数据需要以字节数组的形式,客户端本地构造数据包发给服务端,需要向字节数组里填充各种数据。我发现项目里的代码基本都是如下形式的接口。
GetBytes 这类返回数组的接口,会在函数内 new 出新的字节数组,这会导致额外的 GC,虽然哪怕是 64 位整数都只有区区 8 byte 数据,但是因为这个接口被调用得实在过于频繁,所以还是要对这里进行优化。
优化 GC

这里所要做的事情很简单,仅仅是把数据的字节直接塞进整数数组而不调用 BitConverter ,很简单就能写出如下代码。
上面是反复调用两个整数写入接口 1000000 次所耗费的时间。可以看到性能有显著提升。
使用 Unsafe 代码

上述方式看起来已经有了很明显的性能提升了,但是还能不能更进一步地优化代码呢?答案是可以的,众所周知,代码最后是要翻译成 native code 才能在 CPU 执行,上述代码以访问数组的形式去访问,不如直接一个 32 位整数 一个 mov 指令来得高效。所以可以把代码从对数组的反复赋值,改为对一个整数的赋值,将代码改为如下 unsafe 代码。
代码很简单,就是把字节数组取成指针,然后转型位整数指针,之后赋值,对于写 C/C++ 的人来说应该很熟悉。
性能又提升了一些。不过需要注意的是,代码的编译选项需要是 release 才会体现出性能优势。debug 模式指令不会做出优化。
字节序

上文都没有提及到字节序的问题,在小头端的机器上,需要把数据处理成网络序,简单调整一下代码即可。
unsafe 代码则相对麻烦一些。
这里用优化 GC 的接口性能优势依旧明显。
unsafe 的代码还是有性能提升,但是相对优化 GC 方式的优势不如小头端来得大。
64位整数

然后再写写 64 位整数的版本,首先是 GC 优化。
没啥特别的,简单粗暴地加代码。
Unsafe 代码也很简单。
依旧完爆没有优化过的代码,但是这里可以看到对于 64 位整数,unsafe 代码的优势更加明显了,原理也很简单,因为要做更多的对数组操作。
处理大头端代码时 64 位整数要做的事情也更加复杂,相比起仅仅是优化 GC 的方式,还是有提升的,但是相比起小头端代码的直接赋值,效率逊色不少。
有关 Buffer 类的接口

之前有看到使用 Buffer 类的接口,实际进行的会是类似 memcpy 的直接的字节拷贝的说法,结果我实际试了一下,BlockCopy 并不能带来性能优势,甚至还不如上面那种直接的遍历数组赋值的写法,而 MemoryCopy 确实会带来性能优势,但是比不上上面的优化 GC 的写法,更不用说和 unsafe 的写法比较,而且也要求 unsafe ,实在看不出用这两个接口的好处,也就对可读性友好一些,但个人觉得完全不值得。
总结

C# 提供了很多对用户很友好的接口,其实像最初那种写法,写起来挺舒服的,可读性也很好,但是这些接口往往对性能不是很友好,如果只是少量的调用,那还能接受,但是出现在网络通信这种频繁被调用的模块就受不了了,本文尝试通过代码的改写,对字节数组写入这一操作进行了优化,并简单地进行了分析。


参考

李志敏 发表于 2021-2-19 09:40

不反对造轮子,但是也要了解业内标准Protobuff吧

123456809 发表于 2021-2-19 09:48

Protobuf一般只负责序列化业务层的数据,而消息包里经常需要加消息流水号之类的东西,框架为了对业务隐藏这些细节,就不能用protobuf序列化这些东西,而且也确实还存在直接用json而不是protobuf做序列化的项目[笑哭]

六月清晨搅 发表于 2021-2-19 09:52

为啥要隐藏啊?怕同事学去了吗?

万胜 发表于 2021-2-19 10:01

隐藏的意思是让业务层不用关心实现的细节。。

芊芊551 发表于 2021-2-19 10:08

我觉得没有必要,不是人人都好学,不是人人都有天赋,英伟达的流体代码开源好多年。有多少人能看懂

我是来围观的逊 发表于 2021-2-19 10:16

作者的所说的隐藏细节是表达不同层间封装和代码权限没关系。
页: [1]
查看完整版本: Unity 网络消息打包的优化