大家好,我是飞哥!
在《内存随机也比顺序访问慢,带你深入理解内存IO过程!》一文中,我们理解了内存IO的内部实现过程,知道了内存的随机IO比顺序IO要慢,并对延迟时间进行了大概的估算。那么我们今天来用代码的方式来时间一下,看看在我们的项目工程中,内存访问的在不同的访问场景下延时究竟是个什么表现。
先测顺序情况
测试原理就是定义一个指定大小的double(8字节)数组,然后以指定的步长去循环。这里面的变量有两个。核心代码如下:
void init_data(double *data, int n){
int i;
for (i = 0; i < n; i++) {
data[i] = i;
}
}
void seque_access(int elems, int stride) {
int i;
double result = 0.0;
volatile double sink;
for (i = 0; i < elems; i += stride) {
result += data[i];
}
sink = result;
}
在这个核心代码的基础上,我们有两个可调节变量:
- 一是数组大小,数组越小,高速缓存命中率越高,平均延时就会越低。
- 二是循环步长,步长越小,顺序性越好,同样也会增加缓存命中率,平均延时也低。
我们再测试的过程中采取的办法是,固定其中一个变量,然后动态调节另外一个变量来查看效果。
另外说明一下,这个代码测试中考虑的几个额外的开销的处理情况。
1.加法开销:由于加法指令简单,一个CPU周期就可完成,CPU周期比内存周期要快,所以暂且忽略它。
2.耗时统计:这涉及到高开销的系统调用,本实验通过跑1000次取一次耗时的方式来降低影响。
场景一: 固定数组大小2K,调节步长
步长 | 1 | 9 | 17 | 25 | 33 | 41 | 49 | 57 |
---|---|---|---|---|---|---|---|---|
延时ns | 1.28 | 1.28 | 1.33 | 1.30 | 1.30 | 1.41 | 1.45 | 1.4 |
数组足够小的时候,L1 cache全部都能装的下。内存IO发生较少,大部分都是高效的缓存IO,所以我这里看到的内存延时只有1ns左右,这其实只是虚拟地址转换+L1访问的延时。
场景二: 固定步长为8,数组从32K到64M
数组大小 | 32K | 64K | 256K | 512K | 2M | 8M | 16M | 64M |
---|---|---|---|---|---|---|---|---|
延时ns | 1.27 | 1.73 | 2.03 | 2.62 | 2.62 | 2.88 | 5.17 | 5.84 |
当数组越来越大,Cache装不下,导致穿透高速缓存,到内存实际IO的次数就会变多,平均耗时就增加
场景三: 步长为32,数组从32K到64M
数组大小 | 32K | 64K | 256K | 512K | 2M | 8M | 16M | 64M |
---|---|---|---|---|---|---|---|---|
延时ns | 1.25 | 1.74 | 2.03 | 2.47 | 2.47 | 3.29 | 7.73 | 8.89 |
和场景二相比,步长变大以后,局部性变差,穿透的内存IO进一步增加。虽然数据量一样大,但是平均耗时就会继续有所上涨。不过虽然穿透增加,但由于访问地址仍然相对比较连续,所以即使发生内存IO也绝大部分都是行地址不变的顺序IO情况。所以耗时在9ns左右,和之前估算大致相符!
另外注意一个细节,就是随着数组从64M到32M变化的过程中。耗时有几个明显的下降点,分别是8M,256K和32K。这是因为本机的CPU的L1大小是32K,L2是256K,L3是12M。在数据集32K的时候,L1全能装的下,所有基本都是高速缓存IO。256K的时候、8M的时候,虽然L1命中率下降,但是L2、L3访问速度仍然比真正的内存IO快。但是超过12M以后越多,真正的内存IO就越来越多了。
再测随机IO情况
在顺序的实验场景里,数组的下标访问都是比较有规律地递增。在随机IO的测试中,我们要彻底打乱这个规律,提前随机好一个下标数组,实验时不停地访问数组的各个随机位置。
void init_data(double *data, int n){
int i;
for (i = 0; i < n; i++) {
data[i] = i;
}
}
void random_access(int* random_index_arr, int count) {
int i;
double result = 0.0;
volatile double sink;
for (i = 0; i < count; i++) {
result += data[*(random_index_arr+i)];
}
sink = result;
}
这实际比上面的实验多了一次内存IO,但由于对random\_index\_arr的访问时顺序的,而且该数组也比较小。我们假设它全部能命中高速缓存,所以暂且忽略它的影响。
随机实验场景: 数组从32K到64M
数组大小 | 32K | 64K | 256K | 512K | 2M | 8M | 16M | 64M |
---|---|---|---|---|---|---|---|---|
延时ns | 2.4 | 2.4 | 2.4 | 4.8 | 4.8 | 19.2 | 24 | 38.4 |
这次的数组访问就没有步长的概念了,全部打乱,随机访问。当数据集比较小的时候、L1、L2、L3还能抗一抗。但当增加到16M、64M以后,穿透到内存的IO情况会变多,穿透过去以后极大可能行地址也会变。在64M的数据集中,内存的延时竟然下降到了38.4ns,和我们估算的也基本一致。
结论
有了实验数据的佐证,进一步证实了上一文《深入理解内存IO的实际过程!》的结论。内存也存在随机访问比顺序访问慢的多的情况,大概是4:1的关系。所以不要觉得内存很快,就用起来太随性了!
完整测试代码参见test08
写在最后,由于我的这些知识在公众号里文章比较分散,很多人似乎没有理解到我对知识组织的体系结构。而且图文也不像视频那样理解起来更直接。所以我在知识星球上规划了视频系列课程,包括硬件原理、内存管理、进程管理、文件系统、网络管理、Golang语言、容器原理、性能观测、性能优化九大部分大约 120 节内容,每周更新。加入方式参见我要开始搞知识星球啦、如何才能高效地学习技术,我投“融汇贯通”一票
Github:https://github.com/yanfeizhang/coder-kung-fu
关注公众号:微信扫描下方二维码