2.8.2 巧用struct
struct和class的区别常常被人遗忘,struct结构是值类型,它与class不同的是,struct传递时并不是靠引用(指针)形式而是靠复制,我们可以通俗地认为,它是通过内存复制来实现传递的(真实的情况是通过字节对齐规则循环多次复制内存),也就是说,我们在传递struct时,其实是在不断地克隆数据,其源码如下:
struct A { public int gold; } public void main() { A a = new A(); a.gold = 1; A b = a; b.gold = 2; }
举一个简单的例子,上述struct中有一个整数变量gold,实例a的gold值为1,将a赋值给b后,b的gold设置为2,此时a中的gold依然为1,因为a和b是两个不同的内存。
struct这样的值类型对我们做性能优化有什么好处呢?首先,如果struct被定义为函数中的局部变量,则struct的值类型变量分配的内存是在栈上的,栈是连续内存,并且在函数调用结束后,栈的回收非常快速和简单,只要将尾指针置零就可以了(并非真正意义上的释放内存),这样既不会产生内存碎片,又不需要内存垃圾回收,CPU读取数据对连续内存也非常友好、高效。
除了上述这些,struct数组对提高内存访问速度也有所帮助。我们要明白,由于struct是值类型,所以它的内存与值类型都是连续的,而class数组则只是引用(指针)变量空间连续,这是大不相同的。在CPU读取数据时,连续内存可以帮助我们提高CPU的缓存命中率,因为CPU在读取内存时会把一个大块内容放入缓存,当下次读取时先从缓存中查找,如果命中,则不需要再向内存读取数据(缓存比内存快100倍),非连续内存的缓存命中率比较低,而CPU缓存命中率的高低很影响CPU的效率。
但也不是所有的struct都能提高缓存命中率,如果struct太大,超过了缓存复制的数据块,则缓存不再起作用,因为复制进去的数据只有1个甚至半个struct。于是就有很多架构抛弃了struct,彻底使用原值类型(int[]、bool[]、byte[]、float[]等)连续空间的方式来提高CPU的缓存命中率,即把所有数值都集合起来用数组的形式存放,而在具体对象上则只存放一个索引值,当需要存取时都通过索引来操作数组。我们来看一个例子就知道是怎么回事了。
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类则将数据集中存储在C类中。当两者都对数据进行存取时,A类数据的内存是分散的,因为每次分配A类实例时都是从内存中寻找一块空地来分配,并不保证相邻,arrayA中只是引用连续而非内存连续,而B类数据是内存连续的数组,因为它会将所有同类数据集中在值类型的数组中,值类型的数组分配内存一定是内存连续的,这样就能更好地利用缓存,提高CPU读取数据的命中率。缓存机制是将最近使用过的数据存入最近的空间中,离CPU最近的就是一级缓存和二级缓存,它们是珍贵的,我们应该充分利用它们。