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. Qt之先用了再说系列-多线程方式2

    继Qt之先用了再说系列-多线程方式2 本次说说在Qt里多线程使用第2种方式,也是qt官方比较推荐用的方式,相对与直接继承QThread 重写run方法中灵活了一些,如果第一次使用可能会转不湾来,没有直 ...

  2. 论如何实现最强大脑黑白迭代(c++附代码)

    最近看了最强大脑,对节目中的黑白迭代很感兴趣,就自己写了一个500多行的程序.燃鹅,只实现了一部分功能,还非常简陋.无奈之下,我只好从网上下载了一份代码,然后自己稍加修改就成了一份半改编的代码. 虽然 ...

  3. 收集整理element-ui 表格组件的常用操作方法

    一.简单的表格行内编辑效果 原理是通过Css控制绑定的输入控件与显示值,在选中行样式下对控件进行隐藏或显示. 1.注意下样式的设置 2.change事件 @change="handleEdi ...

  4. Maven报错Missing artifact jdk.tools:jdk.tools:jar:1.7

    1.eclipse中Maven项目的pom文件报错: 2.解决方法: 直接在pom.xml中加上一个依赖项目: <dependency>      <groupId>jdk.t ...

  5. Tun/Tap接口使用指导

    Tun/Tap接口指导 目录 Tun/Tap接口指导 概述 工作机制 创建接口 举例 简单的程序 隧道 拓展 参考 概述 对tun接口的了解需求主要来自于openshift的网络,在openshift ...

  6. Fiddler+模拟器+APP抓包HTTPS 为什么有时候抓不到?

    抓包的原理是什么? 代理 客户端请求 -> 经过代理 -> 到达服务端 服务端返回 -> 经过代理 -> 到达客户端 任何Https的App都能抓到包么? Android7.0 ...

  7. 运用sklearn进行线性判别分析(LDA)代码实现

    基于sklearn的线性判别分析(LDA)代码实现 一.前言及回顾 本文记录使用sklearn库实现有监督的数据降维技术——线性判别分析(LDA).在上一篇LDA线性判别分析原理及python应用(葡 ...

  8. YApi——手摸手,带你在Win10环境下安装YApi可视化接口管理平台

    手摸手,带你在Win10环境下安装YApi可视化接口管理平台 YApi YApi 是高效.易用.功能强大的 api 管理平台,旨在为开发.产品.测试人员提供更优雅的接口管理服务.可以帮助开发者轻松创建 ...

  9. 在laravel中遇到并发的解决方案

    1,在mysql中创建唯一索引,在代码中try catch mysql的1062错误 2.将存在并发的代码丢给队列异步处理.这种解决方案的问题是,接下来的代码不能依赖队列的处理结果 3.使用mysql ...

  10. C++ IO的一些注意点

    读入这个坑一直以来都深受其麻烦,把遇到一些注意点记一下吧. 1.getchar读入 以前练线段树的时候做到Acwing#246 Interval GCD(原题在CodeHunter上,人懒就在Acwi ...