C# Alloc Free编程

首先Alloc Free这个词是我自创的, 来源于Lock Free. Lock Free是说通过原子操作来避免锁的使用, 从而来提高并行程序的性能; 与Lock Free类似, Alloc Free是说通过减少内存分配, 从而提高托管内存语言的性能.

基础理论

对于一个游戏服务器来讲, 玩家数量是一定的, 那么这些玩家的输入也就是一定的; 对于每一个输入, 处理逻辑的时候, 必然会产生一些临时对象, 那么就需要Alloc(New)对象; 然后每次Alloc的时候, 都有可能会触发GC的过程; GC又会将整个进程Stop一会儿(不管什么GC, 都会Stop一会儿, 只是长短不一样); 进而Stop又会影响到输入处理的速度.

这个链式反应循环, 就是一个假设. 只要每个过程产生下一步, 足够多(或者时间长了), 能够维持链式反应. 那么最终的表现就是系统过载. 消费速度越来越慢, 玩家的请求反应迟钝, 进程的内存越来越多, 进而OOM.

如果每个消息处理的耗时比较长, 那么堆积在一起的是输入; 如果每个消息处理的Alloc比较多, 那么堆积在一起的是GC. 这是两个基本的观点.

再回头考虑我们所要解决的问题, 我们要解决一个进程处理5000玩家Online. 那这5000个人, 一秒所能生产的消息数量也就是5000左右个消息, 而我们编程面对的CPU, 一秒处理可是上万甚至更高的数量级. 所以大概率不会堆积在输入这边.

但是Alloc就不一样, 每个业务逻辑消息, 都有其固然的复杂性, 很有可能一个消息处理, 产生了10个小的临时对象, 处理完成后就是垃圾对象. 那么就有10倍的系数, 瞬间将数量级提高一倍. 如果问题再复杂一点呢, 是不是有可能再提高到一个数量级?

这是有可能的!

某游戏服务器内部有物理引擎, 有ARPG的战斗计算, 每个法球/子弹都是一个对象, 中间所能产生的垃圾对象是非常多的, 所以大一两个数量级, 是很容易做到的.

最开始, 我在优化某游戏服务器的时候, 忽略了这一点, 花了很长时间才定位到真正的问题. 直到定位到问题, 可以解释问题, 然后fix掉之后, 整个过程就变得很容易理解, 也很容易理解这个混沌系统为何运行的比较慢.

优化前后的对比

最开始在Windows上面编译, 调试和优化服务器. 以为问题就这么简单, 但是实际上在Linux上面跑的时候, 还是碰到了一点问题.

这是服务器最开始用WorkStationGC跑2500人时候的火焰图, 最左面有很多一块时间在跑SpinLock, 问了微软的人, 微软的人也不知道.

然后当时相同的版本在Intel和AMD CPU下面跑起来, 有截然不同的效果(AMD SA2性能要高一些, 价格要低一些). 以至于以为是Intel CPU的BUG, 或者是其他原因.

WorkStationGCServerGC切换貌似对服务器性能影响也不是很大----都是过载, 机器人开了之后就无法正常的玩游戏, 延迟会非常高.

巧遇XLua

服务器内部有用XLua来封装和调用Lua脚本, 有很多脚本都是策划自己搞定的, 其中包括战斗公式和技能之类的.

我们都知道MMOG的战斗公式会很复杂, 可能一下砍怪, 会调获取玩家和怪物的属性几十次(因为有很多种不同的战斗属性). 然后又是一个无目标的ARPG, 加上物理之类的, 一次砍杀可能会调用十几次战斗公式, 所以数量级会有提升.

XLua在做FFI的时候, 会将对象的输入输出保留在自己的XLua.ObjectTranslator对象上, 以至于该对象的字典里面包含了数百万个元素. 所以调用会变得非常慢, 然后内存占用也会比较高. 这是其一.

第二就是, 每个参数pass的时候, 可能都会产生new/delete. 因为服务器这边字符串传参用的非常多, 所以每次参数传递, 可能都会对Lua VM或者CLR产生额外的压力.

基于这两点原因, 我把战斗公式从Lua内挪到C#内, 然后对Lua GC参数做了相应的调整. 然后发现有明显的提升.

后来的事情

后来的事情就比较简单了, 因为发现减少这次大量的Alloc, 会极大的提高程序的性能. 所以后续的工作重点就放在了减少Alloc上, 然后火焰图上会有明显的对比差别.

这是中间一个版本, 左边pthread mutex的占比少了一些.

这是4月优化后的版本, pthread mutex占比已经小于10%, 可能在5%以内.

而服务器目前的版本, pthread mutex占比已经小于2%. 几乎没有高频的内存分配.

这就是我说的Alloc Free.

现象, 解释和最优化编程

继续回到最开始的那个图, 如果不砍断Alloc, 那么就会GC Stop, 进而就会影响到处理速度.

这是C#在Programming Language Benchmark Game上的测试, 可以看到C#单纯讨论计算性能, 和C++的差距已经不是很大.

而某游戏服务器内, 数百人跑在一个Server进程内, 都会都会出现处理速度不足, 猜想起来核心的问题就在GC Stop. 这是一个业务内找到AllocateString耗时的细节, 其中大部分在做WKS::gc_heap::garbage_collect. 这种情况在WorkStationGC下面比较突出, ServerGC下面也会有明显的问题. 核心的矛盾还是要减少不必要的内存分配, 降到CLR的负载.

当然这个例子比较极端, 从优化过程的经验来看, 10%的Alloc大概有5%的GC消耗. 当一个服务器进程有30%+的Alloc时, 服务器的性能无论如何也上不去.

这是最核心的矛盾. 只有CPU大部分时间都在处理业务逻辑, 才能尽可能的消费更多的消息, 进而系统才不会出现过载现象, 文章最开始说的链式反应也就不会发生.

C#性能的最优化编程

实际上就变成了怎么减少内存分配的次数. 这里面就需要知道一些最基本的最佳实践, 例如优先使用struct, 少装箱拆箱, 不要拼接字符串(而是使用StringBuilder)等等等等.

但是单单有这些还是不够的, 还需要解决复杂业务逻辑内部产生的垃圾对象, 还需要不影响正常业务逻辑的开发. 关于这部分, 在后面一文中会详细讨论, 此处就不做展开.

非托管内存

C#程序内存的分配, 实际上还包含Native部分alloc的内存, 这一点是比较隐性的. 而且由于Windows libc的内存分配器和Linux内存分配器的差异性, 会导致一些不同.

我们在使用dotMemory软件获取进程Snapshot的时候, 可以获得完整托管对象的个数, 数据, 以及统计信息; 但是对非托管内存的统计信息缺没有. 由于服务器在Windows Server上面经过长时间的测试, 例如开4000个机器人跑几天, 内存都没有明显的上涨, 那么可以大概判断出来大部分逻辑是没有内存泄漏的.

Linux上应用和Windows上不一样的, 还有glog的日志上报, 但是关闭测试之后发现也没有影响. 所以问题就回到了, Windows和Linux有什么差异?

带着这个问题搜索了一番, 发现Java程序有类似的问题. Java程序也会因为Linux内存分配器而导致非托管堆变大的问题, 具体可以看Java堆外内存增长问题排查Case.

后来将Linux的启动命令改成:

LD_PRELOAD=/usr/lib/libjemalloc.so $(pwd)/GameServer

之后, 跑了一晚上发现内存占用稳定. 基本上就可以断定该问题和Java在Linux上碰到的问题一样.

后来经过搜索, 发现大部分托管内存语言在Linux都有类似的优化技巧. 包括.net core github内某些issue提到的. 这一点可以为公司后续用Lua做逻辑开发的项目提供一点经验, 而不必再走一次弯路.

参考:

  1. GC Issue
  2. C# Benchmark Game
  3. Java堆外内存增长问题排查

[03] C# Alloc Free编程的更多相关文章

  1. [04] C# Alloc Free编程之实践

    C# Alloc Free编程之实践 上一篇说了Alloc Free编程的基本理论. 这篇文章就说怎么具体做实践. 常识 之所以说是常识, 那是因为我们在学任何一门语言的时候, 都能在各种书上看到各种 ...

  2. 梦织未来Windows驱动编程 第03课 驱动的编程规范

    最近根据梦织未来论坛的驱动教程学习了一下Windows下的驱动编程,做个笔记备忘.这是第03课<驱动的编程规范>. 驱动部分包括基本的驱动卸载函数.驱动打开关闭读取写入操作最简单的分发例程 ...

  3. [连载]JavaScript讲义(03)--- JavaScript面向对象编程

  4. day41-网络编程03

    Java网络编程03 5.UDP网络通信编程[了解] 5.1基本介绍 类DatagramSocket 和 DatagramPacket[数据报/数据包]实现了基于 UDP的协议网络程序 UDP数据报通 ...

  5. 跟着老男孩一步步学习Shell高级编程实战

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://oldboy.blog.51cto.com/2561410/1264627 本sh ...

  6. (转)跟着老男孩一步步学习Shell高级编程实战

    原文:http://oldboy.blog.51cto.com/2561410/1264627/  跟着老男孩一步步学习Shell高级编程实战 原创作品,允许转载,转载时请务必以超链接形式标明文章 原 ...

  7. 如何快速读懂大型C++程序代码

    要搞清楚别人的代码,首先,你要了解代码涉及的领域知识,这是最重要的,不懂领域知识,只看代码本身,不可能搞的明白.其次,你得找各种文档:需求文档(要做什么),设计文档(怎么做的),先搞清楚你即将要阅读是 ...

  8. UITapGestureRecognizer 的用法

    最近在项目中用到了手势操作,键盘回收时还是挺常用的,现在总结下,多谢网络上大神们的分享. 先分享下我在项目中用的代码: UITapGestureRecognizer * mytap=[[UITapGe ...

  9. 20145120 《Java程序设计》第10周学习总结

    20145120 <Java程序设计>第10周学习总结 教材学习内容总结 转自:http://www.cnblogs.com/springcsc/archive/2009/12/03/16 ...

随机推荐

  1. 【MySQL】如何最大程度防止人为误操作MySQL数据库?这次我懂了!!

    写在前面 今天,一位哥们打电话来问我说误操作了他们公司数据库中的数据,如何恢复.他原本的想法是登录数据库update一个记录,结果忘了加where条件,于是悲剧发生了.今天,我们不讲如何恢复误操作的数 ...

  2. Android 开发学习进程0.14 Bindview recyclerview popwindow使用 window类属性使用

    BindView ButterKnife 优势 绑定组件方便,使用简单 处理点击事件方便,如adapter中的viewholder 同时父组件绑定后子组件无需绑定 注意 在setcontentview ...

  3. linux root用户下没有.ssh目录

    .ssh 是记录密码信息的文件夹,如果没有登录过root的话,就没有 .ssh 文件夹,因此登录 localhost ,并输入密码就会生成了 ssh localhost

  4. centos7下的redis集群模式

    1.先安装好单机版的redis 2.Reids安装包里有个集群工具,要复制到/usr/local/bin里去 cd /home/redis/redis-4.0./src ls - cp redis-t ...

  5. JavaScript学习系列博客_20_JavaScript 作用域

    作用域 - 作用域指一个变量的作用的范围 - 在JS中一共有两种作用域 1.全局作用域 - 直接编写在script标签中的JS代码,都在全局作用域- 全局作用域在页面打开时创建,在页面关闭时销毁 - ...

  6. chromium 源码下载地址

    下载链接:https://gsdview.appspot.com/chromium-browser-official/

  7. 第4篇scrum冲刺(5.24)

    一.站立会议 1.照片 2.工作安排 成员 昨天已完成的工作 今天的工作安排 困难 陈芝敏  完成云开发配置,初始化数据库:  线下模块(还剩下获取词的数据库) 倒计时模块的初加载还是有点慢  冯晓凤 ...

  8. Linux MPI环境配置

    参考:https://blog.csdn.net/lusongno1/article/details/61709460 注意点: 1. /etc/profile.d/user.sh和/etc/ld.s ...

  9. Java动态代理(二)——jdk动态代理

    一.什么是动态代理?代理类在程序运行时创建的代理方式被成为动态代理.动态代理的代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的.相比于静态代理, 动态代理的 ...

  10. W3C标准和语义化

    一.语义化的理解 根据内容选择合适的标签,便于开发者阅读,在写出更优雅的代码的同时让浏览器很好的解析. 目的 1.在没有CSS的情况下,页面也能呈现出很好的内容结构和代码结构: 2.有利于SEO:和搜 ...