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

从CPU缓存出发对引用池进行优化

背景

        语言C#,开发工具Unity(不重要)。将从用例一步步进行分析

内容

用例基础

        开发过程中,会碰到一种情况,函数传参为非固定数量,固定类型的值,这里以打开UI界面为例,不同的界面需要的参数是不同的,常用的有两种。

方法一(param object[] 方式)

        参数使用param限制,这里参数有四个,类型分别为【整型,整型,字符串,浮点】,在调用时,直接传入需要的值即可。

pubic void OpenPanel(string mame,params object[] obj)
{}OpenPanel("main",1,2,"haha",2.3f)
方法二(UIParam方式)

        自定义新类型,通过继承以及类型转换的方式,传入以及读取参数。

public void OpenPanel(string name,UIParam param)
{}public class UIParam
{}public class MainParam : UIParam
{public int month;public int day;public string tip;public float value;
}OpenPanel("main",new MainParam()
{month = 1,day = 2,tip = "haha",value = 2.3f
})var mParam = param as MainParam
对比
方法1优点

        灵活性高,不需要再单独为每个界面定义参数类型,实现起来比较快速,代码量很少。

方法1缺点

        类型不安全,编译时无法检查类型是否正常,需要运行时才能确定类型;可读性差;顺序固定,在删除或者插入新数据时,旧的所有地方都需要重新修改顺序;传参时参数内容值类型需要先装箱成引用类型,调用时还需要再拆箱,会产生额外的GC,对性能极其不友好。

方法2优点

        类型安全,经过编译检查;可读性好,根据参数名推断出参数意义,注释方便阅读;维护方便,新增,删除,修改字段比较简单;没有装箱拆箱(字段内没有object类型),类型转换比较轻量,几乎无性能消耗,也不会有GC产生。

方法2缺点

        实现起来比较繁琐,每个界面都需要单独的类型限制参数,代码量较多

总结
对比维度param object[] 方式UIParam方式
灵活性
类型安全高,有编译验证
错误率低,有编译验证
可读性
维护性差,不好插入和删除好,随便修改
开发效率快速开发相对繁琐
装/拆箱频繁,效率低无装/拆箱
GC压力高,object数组以及装箱低,UIParam本身需要分配引用
类型转换成本高,需要手动转换低,需要一次向下转型
调用效率低,取决于实际调用逻辑高,直接调用
总结建议

        快速开发及对性能不敏感的时候,可以使用方法1。否则,使用方法2

关于引用的优化

       继续深入方法2,在总结表里GC压力这一行,可以看到UIParam本身需要分配引用,因为我们传参的时候需要new一个UIParam派生类,此处需要分配引用。针对这一点,可以使用引用池进行优化,当频繁调用OpenPanel时,去引用池获取UIParam实例,而不是重新new一个。简单的代码示例,基础类型定义

public interface IPoolable
{void Reset();
}public class UIParam : IPoolable
{public virtual void Reset() { }
}

        引用池定义

using System;
using System.Collections.Generic;public static class UIParamPool
{private static readonly Dictionary<Type, Stack<UIParam>> poolDict = new();/// <summary>/// 获取一个参数实例/// </summary>public static T Get<T>() where T : UIParam, new(){Type type = typeof(T);if (poolDict.TryGetValue(type, out var stack) && stack.Count > 0){return (T)stack.Pop();}return new T();}/// <summary>/// 回收参数实例/// </summary>public static void Recycle(UIParam param){if (param == null) return;param.Reset();Type type = param.GetType();if (!poolDict.TryGetValue(type, out var stack)){stack = new Stack<UIParam>();poolDict[type] = stack;}stack.Push(param);}/// <summary>/// 清空所有缓存(例如场景切换时)/// </summary>public static void Clear(){poolDict.Clear();}
}

        这样当我们调用OpenPanel时,每次都从引用池获取对象,不需要每次都new,降低了GC的产生。

关于引用池Stack类型的使用

        注意,上面引用池中,字典内部嵌套的类型是栈,这里为什么使用栈,而不是使用队列,或者列表?因为引用池的主要作用可以理解为复用,而栈在复用性上,是最优解。

        这里结合CPU的缓存机制.Net中的对象复用,就很容易明白原因。

CPU缓存机制

CPU缓存命中(Cache Hit)        

        当 CPU 访问内存时,如果所需数据已存在于 L1/L2/L3 缓存中,访问速度会极快;否则就需要从主内存加载,速度慢很多倍。

层级访问速度(大致)缓存大小
L1 Cache非常快(1-4 cycles)小(32KB ~ 128KB)
L2 Cache快(4-14 cycles)中(256KB ~ 1MB)
L3 Cache较慢(几十 cycles)大(几 MB)
RAM慢(100+ cycles)非常大

        即当访问某个变量时,CPU 会先在缓存里查找,如果找到了,就是 Cache Hit;否则就是 Cache Miss,需要去主内存里慢慢找。

        这里拓展下L1/L2/L3的几种模式,不同的CPU采取的缓存模式不同

CPUL1/2/3缓存模式
包容式缓存(Inclusive Cache)
定义

        高层缓存(如 L2/L3)必须包含低层缓存(如 L1)中的所有数据副本,即

        L1 中的数据一定也存在于 L2
        L2 中的数据一定也存在于 L3

优点

        当 L1 中数据被逐出后,L2/L3 一定还保留副本,读取更快,命中率高。

        方便调试和一致性管理(Cache coherence):L3 缓存可看作所有核心共享的总副本。

缺点

        会浪费缓存空间。例如 L1 的 32KB + L2 的 256KB,会重复占用部分空间。

        更难扩展缓存容量,冗余增多。

非包容式缓存(Non-Inclusive Cache)
定义

        高层缓存不保证包含低层缓存的数据,可以重叠也可以不重叠。即

        L1 的数据,L2 可能有也可能没有。

        L2 和 L3 之间也可能不包含彼此数据。

优点

        每一级缓存都能最大化自己的使用空间,不做重复缓存,空间利用率更高。

        更灵活,可以为高并发、专核任务做更好的定制。

缺点

        如果某一级被驱逐数据,下一层不一定能命中 ⇒ 可能直接退到 RAM ⇒ 性能损失。

        一致性维护相对复杂。

排斥式缓存(Exclusive Cache)
定义

        高层缓存(如 L2)与低层缓存(如 L1)完全不允许有重复数据。即

        如果一块数据从 L2 移动到了 L1,就会从 L2 删除。

        如果从 L1 逐出,就重新放回 L2。

        是不是感觉似曾相识,跟C# GC的Generate维护机制很类似

优点

        各层缓存利用率最大。

缺点

        操作复杂,维护代价高。

.Net中的对象复用

        在 C#中,对象是分配在托管堆上的(heap),它们的引用保存在栈上或其他对象的字段中。
引用池复用对象,实际是在避免重复分配/GC,而复用已有对象的内存地址。举例,如下对象

class MyClass
{public int a = 123;public int b = 456;
}

        在内存中是如何存放的,以及CPU是如何缓存的,如下

[ obj 引用(8字节) ]     --> 指向 -->  [ MyClass 实体:a=123 | b=456 ]↑栈或字段            堆

        CPU缓存的是后面那一块“a=123, b=456” 的实际字段值,而不是 obj 本身这个 8 字节引用值。

合并理解

        结合CPU缓存机制以及.Net对象复用机制,我们能很清晰的理解引用池中为什么要用栈结构,因为栈是后进先出,而后进的更有可能存放在CPU的缓存中,CPU能更快的命中这部分内存,如果是使用队列,先进先出,引用池复用的是很久之前的对象,而对象对应的内存数据,可能早就被抛弃到RAM中,触发CPU的Cache Miss,降低执行效率。

拓展

        根据上面的.Net的对象内存存储规则,可以看到内存堆上存放的对象的实际内容,而不是引用,所以我们应该避免在存放数据的对象中使用其他内存量占用较高的数据,比如游戏开发中的贴图,模型等,因为这些的对象整体过大,是无法放到CPU缓存内的,必须要抛弃在RAM中,导致Cache Miss。所以应该对对象进行归类,单纯存放数据,或单纯存放资源,以及资源管理必需的数据,以此来避免资源导致的Cache Miss。

http://www.xdnf.cn/news/682777.html

相关文章:

  • C51-指针函数
  • Linux编译器——gcc/g++的使用
  • 基于Python的智能天气提醒助手开发指南
  • ValueError: BuilderConfig ‘xxxx‘ not found. Available:[xxx]
  • Cannot read properties of undefined (reading ‘clearSelection‘)
  • 华为仓颉语言初识:并发编程之线程的基本使用
  • PCB线路板压合工艺难点解析与技术对策
  • NB-IoT NPUSCH(三)-资源映射
  • gdiplus,GDI +为什么2001年发布后几乎没有再更新了
  • 2025 海外短剧 CPS 系统开发:技术驱动下的全球化内容分销新范式
  • SSM整合:Spring+SpringMVC+MyBatis完美融合实战指南
  • 第十二天 区块链在车辆数据存证中的应用
  • Erp系统介绍与业务方案详情
  • 彻底理解一个知识点的具体步骤
  • 【PP】SAP生产订单(创建-下达-发料-报工-入库)全流程及反向流程
  • VectorNet:自动驾驶中的向量魔法
  • 【Agent】MLGym: A New Framework and Benchmark for Advancing AI Research Agents
  • CVPR2022——立体匹配算法Fast-ACVNet复现
  • 藻华自用数据集学习2025.4.28
  • SPL 轻量级多源混算实践 2 - 查询 csv/xls 等文件
  • 将图层为shapefile类型的文件转成PostGis类型的详细实现步骤
  • 【Linux】cat命令 – 在终端设备上显示文件内容
  • 通用机环境下安全版单机数据库使用非root用户管理的解决方案
  • gbase8s统计更新(UPDATE STATISTICS)介绍
  • redis分布式锁在高并发场景下的方案设计与性能提升
  • 晓辉教育五维乾坤:五个成语解码教育范式革命
  • mysql explain使用
  • 图片压缩工具 | Electron+Vue3+Rsbuild开发桌面应用
  • SecureCRT 和 MobaXterm 用于串口收发时数据异常(无法成功发送)——更改换行符解决
  • OpenResty 入门指南:从基础到动态路由实战