当前位置: 首页 > news >正文

《Unity3D高级编程 主程手记》第二章 C#技术要点(八) 业务逻辑优化技巧

        

目录

使用 List 和 Dictionary 时提高效率

巧用struct

struct 对性能优化的好处

使用原值类型连续空间的方式来提高 CPU 的缓存命中率

尽可能地使用对象池

字符串导致的性能问题

解决方法

字符串的隐藏问题

程序运行原理

业务逻辑的优化方向

脱离 C# 语言,简单陈述程序运行原理

指令内存块

数据内存块


        不只是算法能大幅度提高业务逻辑的效率,普普通通的业务代码也同样可以有质的飞越。

        优秀程序员常关注代码对性能的影响,普通人只关注是否能完成功能,这样日积月累代码质量就会形成差距。

使用 List 和 Dictionary 时提高效率

        List、Dictionary 的实质都是数组。Dictionary有两个数组,一个数组用于保存索引,一个数组用于保存数据,我们遍历 List 和 Dictionary 都是在遍历数组

  1. 如果代码中使用 Insert 、Contains、Remove 函数频率比较高,就会带来不必要的性能消耗。
  2. 注意设置 Dictionary 的初始大小,尽量设置一个合理的大小。
  3. GetHasCode() 是用算法把内存地址转化为哈希值的过程,频繁使用会有算力损耗。可以用唯一 ID(标识)的方式来代替。

巧用struct

        struct 结构是值类型,传递时不是靠引用(指针)形式而是靠复制,是通过内存复制来实现传递(真实情况是通过字节对齐规则循环多次复制内存)。

struct 对性能优化的好处

  1. 不会产生内存碎片,不需要内存垃圾回收
  2. CPU 读取数据对连续内存非常友好、高效
    1. CPU 读取内存时会把一个大块内容放入缓存,下次读取会优先从缓存中查找,如果命中,则不需要向内存中读取数据(缓存比内存快100倍)。连续内存缓存命中率高,非连续内存命中率比较低,CPU 缓存命中率的高低很影响 CPU 的效率。
  3. struct 数组对提高内存访问速度有所帮助
    1. 它的内存和值类型都是连续的,而 class  数组只是引用变量空间连续。

        如果 struct 太大,超过了缓存复制的数据块,则缓存不再起作用。

使用原值类型连续空间的方式来提高 CPU 的缓存命中率

原值类型:int[]、bool[]、byte[]、float[] 等

方法:把所有数值都集合起来用数组的形式存放,而在具体对象上则只存放一个索引值

例子:

class A
{public int a;public float b;public bool c;
}class B
{public int index;
}class C
{private static C _instance;public static C instance{get{if(null == _instance){_instance = new C();return _instance;}return _instance;}}public int[] a = new int{2, 3, 5, 6};public float[] b = new float{2.1f, 3.4f, 1.5f, 5.4f};public bool[] c = new bool{false, true, false, true};
}public void main()
{A[] arrayA = new A[3]{new A(), new A(), new A()};print("A class this is a {0} b {1} c {3}",Aa.a, Aa.ba, Aa.c);B b = new B()b.index = 2;C c = C.instance;print("B class this is a {0} b {1} c {3}",c.a[b.index], c.b[b.index], c.c[b.index]);
}

        A 类数据的内存是分散的,而 B 类数据是内存连续的数据。

        值类型的数组分配内存一定是内存连续的,这样能更好的利用缓存,提高 CPU 读取数据的命中率。

        缓存机制是将最近使用的数据存入最近的空间中,离 CPU 最近的是一级缓存和二级缓存。

尽可能地使用对象池

        内存分配和内存消耗会对程序产生影响。不但要减少内存分配次数和内存碎片,还要避免内存卸载带来的性能损耗。

        因此当我们业务逻辑越庞大数据量越多时,垃圾回收需要检查的内容也越来越多,如果回收后依然内存不足,就得向系统请求分配更多内存。

        我们应该尽可能的用对象池来重复利用已经创建的对象,这有助于减少内存分配时的耗时,也减少了堆内存的内存块数量,最终减少了垃圾回收时带来的CPU损耗

内存分配消耗:

  1. new 操作创建某个类
  2. new List 操作
  3. DIctionary<int,List> 操作

解决:通用对象池

//源自 Unity 的 UI 库中
internal class ObjectPool<T> where T : new()
{private readonly Stack<T> m_Stack = new Stack<T>();private readonly UnityAction<T> m_ActionOnGet;private readonly UnityAction<T> m_ActionOnRelease;public int countAll { get; private set; }public int countActive { get { return countAll - countInactive; } }public int countInactive { get { return m_Stack.Count; } }public ObjectPool(UnityAction<T> actionOnGet, UnityAction<T> actionOnRelease){m_ActionOnGet = actionOnGet;m_ActionOnRelease = actionOnRelease;}public T Get(){T element;if (m_Stack.Count == 0){element = new T();countAll++;}else{element = m_Stack.Pop();}if (m_ActionOnGet != null)m_ActionOnGet(element);return element;}public void Release(T element){if (m_Stack.Count > 0 && ReferenceEquals(m_Stack.Peek(), element))Debug.LogError("Internal error. Trying to destroy object that is already released to pool.");if (m_ActionOnRelease != null)m_ActionOnRelease(element);m_Stack.Push(element);}
}internal static class ListPool<T>
{// Object pool to avoid allocations.private static readonly ObjectPool<List<T>> s_ListPool = new ObjectPool<List<T>>(null, l => l.Clear());public static List<T> Get(){return s_ListPool.Get();}public static void Release(List<T> toRelease){s_ListPool.Release(toRelease);}
}

        对象池并不复杂,麻烦的是使用,程序中所有创建对象实例、销毁对象实例、移除对象实例的部分都需要用对象池去调用。

        在对象池上使用预加载:在程序运行前让对象池中的对象分配得多一些,这样就不需要临时分配内存了。

        资源也可以有对象池:提前将资源内容加载到内存中可以让内存分配次数减少,甚至完全避免临时的加载和分配。

字符串导致的性能问题

        在 C# 中,string 是引用类型,每次动态创建一个 string,C# 都会在堆内存中分配一个内存用于存放字符串。

string strA = "test";
for(int i = 0 ; i<100 ; i++)
{string strB = strA + i.ToString();string[] strC = strB.Split('e');strB = strB + strC[0];string strD = string.Format("Hello {0}, this is {1} and {2}.",strB, strC[0], strC[1]);
}

        注:字符串常量是不会丢弃的。比如这段程序中的 “test”、“Hello {0}, this is {1} and {2}.”

        这段程序中,每次循环都申请5次内存,并且抛弃一次 strA + i.ToString() 的字符串内容。每次循环结束都会将前面所有分配的内存内容抛弃,再重新分配一次,总共申请了500次。

        原因:C# 语言对于字符串没有任何缓存机制,每次使用都需要重新分配 string 内存。

解决方法

  1. 自建缓存机制,使用 Dictionary 容器将字符串缓存起来。
  2. 使用 C# 中的“不安全”的 native 方法,类似于 C++ 的指针方式来处理 string 类。
//方法一
Dictionary<int,string> strCache;string strName = null;
if(!strCache.TryGetValue(id, out strName))
{ResData resData = GetDataById(ID);string strName = "This is " + resData.Name;strCache.Add(id, strName);
}return strName;
//方法二
Dictionary<int,string> cacheStr;public unsafe string Concat(string strA, string strB)
{int a_length = a.Length;int b_length = b.Length;int sum_length = a_Length + b_Length;string strResult = null;if(!cacheStr.TryGetValue(sum_length, out strResult)){//如果不存在sum_length长度的缓存字符串,那么久直接连接后存入缓存。strResult = strA + strB;cacheStr.Add(sum_length, strResult);return strResult;}//将缓存字符串再利用,用指针方式直接改变它的内容fixed(char* strA_ptr = strA){fixed(char* strB_ptr = strB){fixed(char* strResult_ptr = strResult){//将strA中内容拷贝到strResult中memcopy((byte*)strResult_ptr, (byte*)strA_ptr, a_length*sizeof(char));//将strB中内容拷贝到strResult的a_Length长度后面内存中memcopy((byte*)strResult_ptr+a_Length, (byte*)strB_ptr, b_length*sizeof(char));}}}return strResult;
}public unsafe void memcopy(byte* dest, byte* src, int len)
{while((--len)>=0){dest[len] = src[len];}
}

字符串的隐藏问题

        字符串的隐藏问题涉及 ToCharArray、Clone、Compare 等内容。

        string.ToCharArray() 返回的 char[] 是一个新创建的字符串数组。

        string.Clone、string.ToString 接口,并不会重新构建一个 string,而是会直接返回当前的 string 对象。

字符串比较:

  1. 判断引用是否相等
  2. 判断长度是否相等
  3. 遍历字符串,判断每个字符是否相等

        如果两个字符串来自不同的内存段,那么在比较它们是否相等时就会遍历所有字符来判断是否相等。

string 源码地址:https://referencesource.microsoft.com/#mscorlib/system/string.cs

程序运行原理

业务逻辑的优化方向

  1. 如何利用好 CPU 缓存命中率
  2. 如何减少内存分配和卸载次数
  3. 何如利用好多线程,让每个线程协作顺畅、并且能分担任务

脱离 C# 语言,简单陈述程序运行原理

        一个程序在内存中运行时,通常由几个内存块组成。

指令内存块

        里面存储的都是已经编写设计好的执行的指令,需要执行的指令都会从指令内存块中去取,指令计数器也不断跳跃在这些指令中。

数据内存块

        里面存放的都是我们设置好的数据以及分配过的内存。

        静态数据块,通常里面存放的都是不变的数据,比如字符串常量,常量整数,常量浮点数,以及一些静态数据,这些数据在程序启动时最先被放入内存中。

        堆内存数据块,所有的动态内存申请都来自堆内存,我们可以认为它是一个很长的 byte 数组,当我们申请内存时,会从数组中找出一块我们指定大小的内存,这个内存不一定是空的,因为内存回收从来不会对内存单位有清理操作,那样太浪费算力了,从来都是将这段数据的指针回收或偏移。所以实际上,我们申请的内存块,在没初始化前都是未知的,有可能刚好前面用过的与我们相似的内容,如果不进行初始化,就有可能会出现逻辑问题

        这里还有个系统层,我们在系统层面上运行程序,所以遵循的是系统层面的逻辑。操作系统提供了虚拟地址,以此避免程序直接与硬件打交道。包括 iOS 和安卓,日常分配的都是虚拟内存。

        我们用惯类对象很容易以为内存中就是某个类的实例内存,其实在机器指令和内存中可没有这个说法,它只是块连续 byte 内存单元,具体其代表哪个类的实例只是我们想象的而已。

        类的方法或函数被编译成指令序列,放在了指令内存块中,所有的方法,函数都在那里集中存放着,随时能取到。

        一个可执行文件或程序库里,几乎都是指令机器码,以及指令附带的常量数据,如果常量比较多可执行文件也跟着会变大。可执行文件和库被装入内存成为指令段内存,里面装着所有类的方法或者函数,包括静态、公共的、私有的等,只是名字上不同,它以名字来区别共有公有的还是私有的,比如 Class_A_public_GetData 可以认为是类对象 A 的 public 方法的 GetData,这个函数只是代表指令的地址

        栈内存块,通常都是函数方法执行的重要部分,它与堆内存不同的是它是有秩序的,只允许遵守先进后出的规则。

        我们所说的值类型数据大都在栈中分配,除非它被用于构造其他类型,比如类、数组等。

        上述其实就是汇编里的数据段、代码段、栈段,它们分别使用了段地址和偏移量来表示数据和指令内容。

  • 当指令数据需要数据段内容时,就用数据段地址 + 偏移量去存取数据内存中的数据,
  • 当指令跳转时则使用代码段地址 + 偏移量来指向新的指令内存地址,
  • 当需要用到栈时则使用 pop 和 push 的汇编指令来偏移栈顶指针从而存取栈上的数据。

        除了内存,寄存器是离 CPU 最近也最快的存储单元,它一般都用来临时存放数据的,当然我们也可以自己写汇编让,某些寄存器长期存放一些数据,以加快读取某数据的速度。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【C#】计算两条直线的交点坐标
  • 升级TrinityCore 服务器硬件
  • 内网隧道——隧道技术基础
  • Qt Creator:C++与Python混合编程
  • (21)起落架/可伸缩相机支架
  • C语言:进程间通信
  • Linux(openwrt)下iptables+tc工具实现网络流量限速控制(QoS)
  • 基于面向对象和递归的拦截器设计模式
  • 1.24、定义浅层神经网络架构和算法(matlab)
  • Android11 framework 禁止三方应用开机自启动
  • 正则表达式在Python中的高级应用:从HTML中提取数据
  • c++应用网络编程之四Linux常用的网络IO模型
  • WPF之URI的使用
  • Linux 各目录
  • MySQL-显示所有错误信息
  • [译] 怎样写一个基础的编译器
  • C++入门教程(10):for 语句
  • Invalidate和postInvalidate的区别
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • mac修复ab及siege安装
  • mockjs让前端开发独立于后端
  • PHP 7 修改了什么呢 -- 2
  • python 学习笔记 - Queue Pipes,进程间通讯
  • Solarized Scheme
  • spring学习第二天
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 闭包--闭包作用之保存(一)
  • 搞机器学习要哪些技能
  • 构建二叉树进行数值数组的去重及优化
  • 前嗅ForeSpider中数据浏览界面介绍
  • 手机端车牌号码键盘的vue组件
  • 数据科学 第 3 章 11 字符串处理
  • 微信如何实现自动跳转到用其他浏览器打开指定页面下载APP
  • 国内唯一,阿里云入选全球区块链云服务报告,领先AWS、Google ...
  • ​LeetCode解法汇总2304. 网格中的最小路径代价
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • # 移动硬盘误操作制作为启动盘数据恢复问题
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • ( 用例图)定义了系统的功能需求,它是从系统的外部看系统功能,并不描述系统内部对功能的具体实现
  • (35)远程识别(又称无人机识别)(二)
  • (C++)八皇后问题
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (计算机网络)物理层
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (四)Linux Shell编程——输入输出重定向
  • (图)IntelliTrace Tools 跟踪云端程序
  • (正则)提取页面里的img标签
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包
  • (转)关于pipe()的详细解析
  • ***检测工具之RKHunter AIDE
  • ./mysql.server: 没有那个文件或目录_Linux下安装MySQL出现“ls: /var/lib/mysql/*.pid: 没有那个文件或目录”...
  • .libPaths()设置包加载目录
  • .NET 5种线程安全集合
  • .NET Core 项目指定SDK版本