Skip to content

CPU侧性能优化

这一节主要是描述在typescript侧,而非C++侧的经验之谈。

  • 以何种形式来进行操作:以一个三维向量举例,即Vec3,往往会在3D引擎的操作过程中大量的使用。那么有几种实现形式。

    typescript
    // 直接使用原生数组,方法使用静态方法,或者挂接到该数组对象的proto中去
    const vArray = [0, 0, 0];
    
    // 直接使用原生对象,方法使用静态方法,或者挂接到该数组对象的proto中去
    const vObject = { x:0, y:0, z:0 };
    
    // 中规中矩的使用类对象,方法就是使用类的成员函数
    class Vec3 {
        x: number = 0;
        y: number = 0;
        z: number = 0;
    
        add(other:Vec3) {
            this.x += other.x,
            this.y += other.y,
            this.z += other.z;
        }
    }
    const vClass = new Vec3();

    以上三种方式那种更好呢,经过实验测试以类的形式效率最好,以object的形式次之,数组的形式最差。 这主要的原因,是因为JavaScript是一个基于prototype的一种语言:

    • 如果以类的形式定义,那么在对象初始化初期,就已经确定了xyz三个属性
    • 如果以object定义,那么是动态的属性访问了xyz
    • 如果以数组形式,那么动态的访问了以number为key的动态属性。而数组的实现是基于object的再封装。
  • 对象是否需要缓存池来进行管理:依然以Vec3举例,new出来的vec3要不要缓存起来,下次再用?实际上这里不能一概而论,我们需要区分该对象是否再逻辑上是一个重度对象。

    • 对于Vec3来说,不需要写缓存池,放入缓存池中管理还有可能更慢。得益于v8引擎的内存管理器,大名鼎鼎的jcmalloc,已经解决了应用程序内存与系统内存的吞吐问题,一般程度的(如Vec3)对象分配相对于上G的底层内存分配器来说都是小打小闹,所以上层无需再封装池管理;为什么有可能会更慢呢,因为池管理封装的时候,可能要把回收的对象放到数组里面,放到链表里面,或者什么结构。那么插入删除的操作就会带来不小的开销,在大批量的复杂情况下,会直接带来帧率的下降和感官上的卡顿。
    • 对于一些重度对象,比如有一个很大的资源缓存了buffer,或者是一个GPU句柄,其背后管理了一个显存对象,那么可以使用池,池管理的并不是js对象本身,而是其背后的资源。
  • 内存密集的操作是否依然有效。现在CPU分为多级缓存,所以引入CacheMissing的概念,一旦发生CacheMissing,那么就会伴随性能上的降低。如果尽量使用使用ArrayBuffer来存储数据,是否能够规避CacheMissing以提升性能呢?因为js底层的内存分配器已经帮助我们做了内存管理,所以即使是不相关的对象其本身也可能在内存上挨在一起,这一部分并不需要特殊和特别的考虑。我个人觉得使用ArrayBuffer确实能够带来内存密集的形式,但是考虑到业务的形式,并不一定非常适用,所带来的性能提升也会因为底层内存分配器的原因而打折扣(已经足够快了,写成ArrayBuffer也可能无法更快)所以还是该怎么写就怎么写,不需要统一到某一种形式上来。

    • 这里可以顺便提一句ECS模式,这种模式主打的就是数据与方法分离,因为数据在一起,可能是用SOA来替代AOS,达成内存密集的存在形式,但实际上在js中并不适用,在C++引擎底层更加适用。本身业务也很少有大规模遍历数据的形式,有也不怕,有js底层分配器兜底。
  • 引用查找带来的性能降低。在复杂系统的开发中,往往是管理器持有对象,对像再持有子对象,所以当访问一个对象的时候,可能要访问好多层,如obj1.property1.info1.sub1.object,一路点过去或一路中括号。或者是有命名空间套命名空间,一路点过去找到了对应的类或函数。其实这种级联访问也是带来不小的性能损失,尤其是在密集操作的时候。印象中对这里的优化提升了10多帧。

  • 使用Object还是使用Map,一些帖子会有二者在适用形式上的对比,如Map可以以任何对象做key,而object则必须是string或者number等,或者在何种使用场合选择使用那种形式。但是很少说出于性能的原因,而使用何种形式。这里贴出一个链接,帖子中做了二者在性能上的对比,但是我并不完全相信其实验结论,因为往往实验过程和他们持有的对象个数有关,如持有1千万个和持有十亿个的区别,一些在C++侧的实现表明,一旦遍历的Size超出1级缓存、2级缓存会带来明显的下降。而又因为又底层分配器的原因,又因为使用场景和业务的原因,这一结论应该是模糊的。