[开发者心得] 【性能教室】通过降低GC、利用CPUcache、资源管理来优化性能

[复制链接]
2157 |2
空伊伊 发表于 2023-4-21 14:30:51 | 显示全部楼层 |阅读模式
GC、利用CPUcache、资源管理
所使用编辑器版本:Online_v0.24.0.0

一、GC
垃圾回收机制(Garbage collection):当需要分配的内存空间不再使用的时候,VM将调用垃圾回收机制来回收内存空间。
(简单理解:当一个对象不再被引用的时候,这个对象占用的内存将会被回收)

GC频率会影响游戏的帧率表现。我们需要通过降低内存申请、内存释放的操作次数,来降低GC频率,从而优化帧率表现。
常用的优化方式有:复用外层作用域变量;对象池

复用外层作用域变量
可以将被“大括号{ }”包裹的代码区域称为一个作用域,这样就能分辨出“最外层”、“最内层”。
代码作用域有这两个特点:
1.“内层”作用域能访问“外层”作用域的变量(“外层”不能访问“内层”)
2.在“内层”作用域内申明的变量,如果“外层”不对它进行引用,那么这个变量将会被GC。(因为外部没引用它的话,计算机会认为这个变量只需要在内层作用域使用一次,使用完相当于变成了垃圾,自然就要进行回收了)

举个最常见的例子,在update中改变物体的Location,是不会每帧都去new一个新的Vector的,而是事先声明一个Vector,然后进行重复使用
image.png
(不进行复用,每一帧都会创建一个新的Vector,会频繁触发GC)
image.png
(进行复用,每帧使用的是最开始创建的Vector)


对象池
对象池是游戏开发中最常用的优化手段,当有大量需要重复创建和销毁的对象时,可以使用对象池来进行管理
(这一节马上会有专题进行介绍,记得先关注!)



二、利用CPUcache
CPU Cache是位于CPU和内存之间的临时存储器,它的容量比内存小很多但速度极快,可以将内存中的一小部分加载到Cache中,当CPU需要访问这一小部分数据时可以直接从Cache中读取,加快了访问速度。

以一个战斗游戏来举例,战斗中存在大量的需要每帧更新的逻辑(位置、伤害计算、AI、避障计算等等)。如果能在数据排列上能够保证内存的紧邻,利用CPU cache,可以大幅提高帧率数据,降低cache miss导致的不必要的消耗。
尽管脚本代码在实际运行时,会出现乱序存储、vm执行过程中大量读写逻辑以外的数据。这些操作都会破坏cache。而且在业务落地中,很难设计出cache友好类型的数据结构。不过在一些原子逻辑的data-locality模式相比无序创建还是会提高一些运行速度。
image.png
(不使用 data-locality 模式下 每个逻辑帧CPU运行的示意图)

image.png
(使用 data-locality 模式下 每个逻辑帧CPU运行的示意图)

实际测试:
紧凑模式迭代:按“行”迭代一个数组(一行在数组中是连续的)
松散模式迭代:按“列”迭代一个数组(一列在数组中相差一行距离)
image.png
测试代码:
class Boom {
    public x: number;
    public y: number;
    constructor(public id: number) {
    }
    setPosition(x, y) {
        this.x = x;
        this.y = y;
    }
}

const ROWS = 1000;
const COLS = 1000;
const repeats = 100;
const arr = new Array(ROWS * COLS).fill(0).map((a, i) => new Boom(i));


function localAccess() {
    for (let i = 0; i < ROWS; i++) {
        for (let j = 0; j < COLS; j++) {
            arr[i * ROWS + j].x = 0;
        }
    }
}


function farAccess() {
    for (let i = 0; i < COLS; i++) {
        for (let j = 0; j < ROWS; j++) {
            arr[j * ROWS + i].x = 0;
        }
    }
}

function repeat(cb: Function, type: string) {
    console.log(`开始${type}迭代`);
    const start = Date.now();
    for (let i = 0; i < repeats; i++) {
        cb();
    }
    const end = Date.now();
    console.log(`完成${type}迭代,耗时:` + ((end - start) / 1000).toFixed(4) + '秒');
    return end - start;
}

//repeat(localAccess, "紧凑模式")
repeat(farAccess, "松散模式")



三、资源管理
我们游戏战斗中会使用大量的资源(模型,特效,音效),可以建立了一个统一管理所有资源 加载/释放的AssetMgr,并通过打印数据调试,确定每个资源的使用频率,最大缓存个数,缓存命中率等参数,调整各个资源的优先加载权重,预创建数量,缓存个数等参数,降低游戏过程中因创建资源/销毁对象等不必要的开销,来尽可能地达到内存和cpu的平衡。
举两个例子:
情况一
有一份资源,只在游戏刚开始时需要的比较多,会达到一个峰值,往后就使用的比较少了。所以可以通过提前计算好资源的实际情况,来进行创建和销毁,而不是让资源一直占用内存。
情况二
有一份资源,在游戏中途需要进行大量使用,那么便可以在游戏刚开始的时候就逐步进行创建,而不是等到了要使用的时候再大量创建,这样可以减少卡顿。

资源管理的可操作性比较灵活,需要对自己的游戏进行实际分析后再来执行相应的操作。内存和CPU的使用情况可以使用Devtool来进行分析
image.png
回复

使用道具 举报

(๑•ั็ω•็ั๑) 发表于 2023-4-21 15:04:51 | 显示全部楼层
牛逼啊,学到唠
回复

使用道具 举报

窜稀大仙 发表于 2023-5-13 22:51:46 | 显示全部楼层
data-locality模式是不是有点ECS框架的感觉
回复

使用道具 举报

热门版块
快速回复 返回顶部 返回列表