SoA vs AoS

SoA vs AoS

今天我们聊一点内存中数据组织的话题,题目中提到了两个名词array of structures (AoS)和structure of arrays (SoA),这分别代表着两种不同的数组组织形式。比如下面一段代码:
    
    
struct AoSData { public int a ; public int b ; public int c ; public int d ; } struct SoAData { public NativeArray < int > aArray ; public NativeArray < int > bArray ; public NativeArray < int > cArray ; public NativeArray < int > dArray ; }
他们的数据在内存中的布局就如下图:
从图中不难看出 AoSData 的数据 a , b , c , d 在内存中是交错存在的。而 SoAData 中相同的数据在内存中是连续存在的。
可能大家从刚接触编程开始,一直会使用 AoSData 这种形式,这确实是普遍使用的一种数组组织形式,因为这符合大家的思维习惯而且天然的被大多数编程语言所支持。不知道大家在学习编程的过程中是否思考过这种数据组织形式的优劣,不妨让我们一起结合前几篇文章的内容来一起思考一下:
这里假设 a , b , c , d 四个数据的大小是一样的并且当前CPU的cache line一次可以加载4个数据即一个cache line可以加载 a , b , c , d 或者 a , a , a , b 四个数据。
让我们一起来看一下在使用 AoSData 的情况下我们要访问所有的 a 数据(使用上图的内存数据)CPU需要访问几次内存:
第一次:加载abcd,找到第一个a
第二次:加载abcd,找到第二个a
第三次:加载abcd,找到第三个a
使用 AoSData 的情况下,我们一共访问了3次内存。
再来看一下使用 SoAData 的情况:
第一次:加载aaab,找到三个a
很明显 SoAData 对数据的访问效率更高,CPU的执行效率自然也更高。还有另外一点,就是SoA对SIMD指令更加友好,Burst对SoA形式的数据也有特别的支持,这一点我们下一章会聊到,这里暂时就不展开了。
OK,Demo时间到了,这次我们来模拟Unity中的Transform,通过AoS和SoA两种数据定义来对比一下他们对性能的不同影响,先给出两种数据定义:
    
    
public struct TransformAoS { public float3 position ; public quaternion rotation ; public float3 scale ; } public class TransformSoA { public NativeArray < float3 > positions ; public NativeArray < quaternion > rotations ; public NativeArray < float3 > scales ; }
以上就是Unity中典型的AoS和SoA数据的组织形式了,接下来我们来看一下AoS的Job实现:
    
    
public struct TransformAoSJob : IJobFor { public NativeArray < TransformAoS > transformAoSes ; [ ReadOnly ] public float3 velocity ; [ ReadOnly ] public float deltaTime ; public void Execute ( int index ) { var transAoS = transformAoSes [ index ] ; transAoS . position += velocity * deltaTime ; transformAoSes [ index ] = transAoS ; } }
这里有一个比较值得注意的点, var transAoS = transformAoSes[index]; 这一句中,因为 TransformAoS 是struct类型,当赋值时,会产生一次结构体拷贝(struct blit),在结构体比较小的时候性能开销并不高,但是当结构体中数据量比较大的时候,值拷贝产生的性能开销就不能忽略不计了。另外一个问题是, transAoS 既然已经是一个拷贝了,我们对他的修改就不会反映在数据本体上,所以在Job的最后我们把拷贝出来的值又赋值回了transformAoSes数组,这样就又产生了一次数据拷贝。有没有更好的办法么?答案当然是肯定的。
Unity在 NativeArrayUnsafeUtility 中为我们提供了 GetUnsafePtr() 方法,我们可以利用指针来获取原始数据的引用,代码可以改成如下形式:
    
    
public struct TransformAoSJob : IJobFor { public NativeArray < TransformAoS > transformAoSes ; [ ReadOnly ] public float3 velocity ; [ ReadOnly ] public float deltaTime ; public void Execute ( int index ) { // var transAoS = transformAoSes[index]; // transAoS.position += velocity * deltaTime; // transformAoSes[index] = transAoS; var transformPtr = ( TransformAoS * ) transformAoSes . GetUnsafePtr ( ) ; ref var transform = ref transformPtr [ index ] ; transform . position += velocity * deltaTime ; } }
OK,我们再来看一下SoA的Job实现:
    
    
public struct TransformSoAJob : IJobFor { [ NoAlias ] public NativeArray < float3 > positions ; [ ReadOnly ] public float3 velocity ; [ ReadOnly ] public float deltaTime ; public void Execute ( int index ) { positions [ index ] += velocity * deltaTime ; } }
SoA形式的Job代码就简洁多了,因为我们可以直接以最高效率访问需要的数据,代码也更加纯粹。(同样也需要注意struct blit的问题)
代码有了,让我们对比一下两者的性能表现吧。
哈?拉了拉了。。扯了半天就这?这俩简直就是一模一样好吧。。
我只能说这口锅只能mono背了,为了证明我没有错,是时候请出老大哥 Burst 了。我们给这两个Job加上 [BurstCompile] 属性再来测试一下:
请忽略Burst给我们带来的 15倍 性能提升(手动狗头)。经过Burst编译之后的代码可以充分看出SoA相较于AoS的性能优势。
简单总结一下,相较于AoS,SoA更能发挥Burst的性能优势。
从Profiler数据中我们可以看到SoA相较于AoS有着300%的性能提升,300%似乎也不是很值得我们改变已经很熟悉的AoS书写习惯嘛,如果你也是这么想的,下一章我们一起来把这个数字提升到700%,是不是很有吸引力了呢?😁
【文章目录】
  1. SoA vs AoS
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值